[JAVA] 나만의 ENUM 사용기

@JsonFormat, @JsonValue, Enum 데이터 그룹 관리 + 활용기

Posted by iheese on October 25, 2022 · 10 mins read
  • 모든 실습은 IntellJ 에서 진행되었습니다.
  • 진행했던 프로젝트의 코드를 기반으로 예시를 작성하여 다를 수 있습니다.


  • 프로젝트를 진행하면서 테이블을 만들 때 많이 고민했던 부분이 필드의 타입이었다. 연관된 상수들, 즉 어떤 집합 중 하나에 속하는 경우가 굉장히 많았다.(예시: 나이대(10대, 20대, 30대, …), 직업(공무원, 학생, 무직, …))
  • String 타입으로 받게 되면 정해진 값이 아닌 다른 값이 들어와도 모르는 단점이 있고, 프론트엔드와 백엔드가 협업할 때 바뀌는 값을 정확하게 매번 업데이트해주지 않으면 문제가 생길 수 있다.
    • 정해진 값으로 DB에 넣어줘야 조회할 때도 정해진 값으로 조회가 될텐데 값이 달라지면 조회가 안되게 된다.
  • 고정된 상수의 열거형인 Enum으로 만들어 정해진 값으로 받으면 문제가 해결될 수 있고 IDE의 도움도 받을 수 있게 된다.


ENUM

@Entity
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private JobStatus jobStatus;
}
public enum JobStatus {
        STUDENT,
        EMPLOYED,
        NOTEMPLOYED
    }

  • Entity 클래스 안에 Enum 클래스를 만들 수도 있고, 따로 클래스로 빼서 만들기도 한다.
  • STUDENT, EMPLOYED, NOTEMPLOYED 직업 상태에 대한 상수의 열거형이며 타입으로 선언할 수 있다.


@RequiredArgsConstructor
public enum JobStatus {
        STUDENT("학생", "student"),
        EMPLOYED("취업성공", "employed"),
        NOTEMPLOYED("취업준비", "notemployed");

        private final String koreanName;
        private final String lowerCase;

      //JobStatus(String koreanName, String lowerCase){
      //      this.koreanName=koreanName;
      //	  this.lowerCase=lowerCase;
      //} 생략가능!
    }
  • 필드를 추가해 상수와 관련된 값들을 묶을 수 있으며, 필드 선언 순서에 따라 해당 필드값을 가리킨다. (“학생” -> koreanName, “lowerCase” -> “student”)
  • 필드를 추가하면 생성자를 생성해주어야 하며, lombok의 @RequiredArgsConstructor를 이용하면 생략이 가능하다.


@JsonFormat, @JsonValue

@JsonFormat
@RequiredArgsConstructor
public enum JobStatus {
        STUDENT("학생"),
        EMPLOYED("취업성공"),
        NOTEMPLOYED("취업준비");

		@JsonValue
        private final String koreanName;
    }
  • @JsonFormat은 속성값을 직렬화하는 방법에 대한 세부 정보를 구성하는 데 사용되는 범용 주석이다.
  • 직렬화하는 것에 대한 타입을 정할 수 있다. 아래 말고도 많은 타입을 정해줄 수 있다.
    • shape = JsonFormat.Shape.OBJECT
    • shape = JsonFormat.Shape.STRING
    • shape = JsonFormat.Shape.NUMBER
  • 원래 아무 표시도 해주지 않으면 enum의 직렬화 결과는 상수의 이름이지만 @JsonFormat, @JsonValue으로 정해주면 @JsonValue로 정해준 필드로 직렬화가 된다.
  • 예시: 위처럼 Enum을 만들면 입력받을 때도 “학생” 으로 받게 되고, 출력될 때도 “학생”으로 출력되게 된다. 그러나 DB 상에는 상수값인 STUDENT 로 저장된다.
  • @JsonFormat의 Enum의 파트의 설명을 보면 String, Number, Json Objects로 직렬화가 가능하지만 역직렬화는 지원하지 않는다고 한다.


ENUM에 메소드 추가

  • 위 방법은 편리하지만 치명적인 단점이 있다. 상수값으로 ENUM을 조회하면 직렬화되기 때문에 찾지 못한다. (koreanName 값으로 조회해야 한다.) 그 대안으로 사용했던 방법을 기록한다.
@Getter
@JsonFormat
@RequiredArgsConstructor
public enum JobStatus {
        STUDENT("학생"),
        EMPLOYED("취업성공"),
        NOTEMPLOYED("취업준비");
        EMPTY("빈값");

		@JsonValue
        private final String koreanName;
        
        public static JobStatus findByJobStatus(String input){
        	return
                Arrays.stream(JobStatus.values())
                        .filter(jobStatus -> jobStatus.getKoreanName().equals(input))
                        .findAny()
                        .orElse(EMPTY);
    }
}
  • ENUM의 values() 메소드는 Enum 상수들을 선언한 순서대로 Array 배열로 리턴해준다.
  • 상수의 한국말과 입력 받은 input 값을 비교해 일치하면 해당 상수를 리턴하고 없으면 EMPTY 상수를 리턴한다.
  • DB에는 상수로 저장되어 있으므로 위 메소드를 통해 조회하여 해당 객체를 조회할 수 있다.


Enum 데이터 그룹 관리

  • 우아한 형제셨던 이동욱님(현 인프런CTO님)의 Enum 활용기를 참고하여 작성하였다.
  • 어떤 데이터가 들어왔을 때 어떤 Enum의 목록에 속하는지 확인하고 해당 Enum의 상수를 리턴하거나 분류하는 방법이다.
@Getter
@RequiredArgsConstructor
public enum JobStatus {
    STUDENT("학생", Arrays.asList("HIGH_SCHOOL", "UNIVERSITY")),
    EMPLOYED("취업성공", Arrays.asList("SALARY_MAN", "SELF_EMPLOYED", "ETC")),
    NOTEMPLOYED("취업준비", Arrays.asList("CHALLENGING", "STUDYING", "GIVING_UP")),
    EMPTY("빈값", Collections.EMPTY_LIST);

    private final String koreanName;
    private final List<String> detailList;

	//String으로 들어온 값을 확인하고 해당 상수를 리턴한다. 
    public static JobStatus findByDetail(String input){
        return Arrays.stream(JobStatus.values())
                .filter(detail -> detail.hasDetailList(input))
                .findAny()
                .orElse(EMPTY);
    }

    public boolean hasDetailList(String input){
        return detailList.stream()
                .anyMatch(detail -> detail.equals(input));
    }
}
  • 한 Enum 상수 안의 필드로 List를 넣어 해당 List에 속하는지 해당 Enum에게 물어보게 하는 메소드이다.


public class JobStatusTest {

    private String selectedDetail(){
        return "HIGH_SCHOOL";
    }

    @Test
    public void jobStatusTest(){
        //given
        String detail = selectedDetail();

        //when
        JobStatus jobStatus = JobStatus.findByDetail(detail);

        //then
        assertEquals(jobStatus.name(), "STUDENT");
        assertEquals(jobStatus.getKoreanName(), "학생");
    }
}
  • 위처럼 상수들에 속한 List의 String 값 중 어떤 JobStatus에 속하는지 알 수 있게 된다.
  • List의 String 값들을 Enum 타입으로 강제 하는 방법도 있다.


@Getter
@RequiredArgsConstructor
public enum Detail {

    HIGH_SCHOOL("고등학생"),
    UNIVERSITY("대학생"),
    SALARY_MAN("직장인"),
    SELF_EMPLOYED("자영업자"),
    ETC("기타"),
    CHALLENGING("취업도전중"),
    STUDYING("취업공부중"),
    GIVING_UP("포기상태");

    private final String koreanName;
}

@Getter
@RequiredArgsConstructor
public enum JobStatus {
    STUDENT("학생", Arrays.asList(Detail.HIGH_SCHOOL, Detail.UNIVERSITY)),
    EMPLOYED("취업성공", Arrays.asList(Detail.SALARY_MAN, Detail.SELF_EMPLOYED, Detail.ETC)),
    NOTEMPLOYED("취업준비", Arrays.asList(Detail.CHALLENGING, Detail.STUDYING, Detail.GIVING_UP)),
    EMPTY("빈값", Collections.EMPTY_LIST);

    private final String koreanName;
    private final List<Detail> detailList;

	//타입을 변경해 타입 안정성을 확보한다. 
    public static JobStatus findByDetail(Detail inputDetail){
        return Arrays.stream(JobStatus.values())
                .filter(detail -> detail.hasDetailList(inputDetail))
                .findAny()
                .orElse(EMPTY);
    }

    public boolean hasDetailList(Detail inputDetail){
        return detailList.stream()
                .anyMatch(detail -> detail.equals(inputDetail));
    }
}
public class JobStatusTest {
   private Detail selectedDetail2(){
        return Detail.STUDYING;
    }
    
    @Test
    public void jobStatusTest2(){
        //given
        Detail detail = selectedDetail2();

        //when
        JobStatus jobStatus = JobStatus.findByDetail(detail);

        //then
        assertEquals(jobStatus.name(), "NOTEMPLOYED");
        assertEquals(jobStatus.getKoreanName(), "취업준비");
    }
  • 앞선 코드와 거의 비슷한 내용이지만 클라이언트에서 전달될 값을 고정시킬 수 있다는 점이 타입 안정성을 확보할 수 있게 된다.


나중에 사용할 것 같아서 정리하는 Enum 활용기

  • 아래 Reference에 자세한 코드와 설명을 참고해주세요.

  • Enum을 Value 클래스로 리턴하기
    • 일관된 타입을 받기 위해 Interface를 생성하고 해당 Interface를 VO객체를 생성자 인자로 받아 인스턴스를 만든다.
    • Enum 역시 해당 Interface를 구현한다.
    • 그리고 Enum을 Value 클래스로 변환하여 Enum의 상수값과 상수의 필드값을 JSON 리스트 형태로 리턴한다.
  • Enum.values 반복하는 과정 줄이기
    • LinkedHashMap 기반의 Enum을 담을 팩토리 클래스를 만든다.
    • 팩토리 클래스를 Bean 등록하고 Enum을 저장한다.
    • View Layer 같이 필요한 곳에서 꺼내어 사용한다.


Reference: