Java Enum 활용기(기본/활용/Spring/MyBatis/테스트까지)
목차
- Enum 설명
- Enum 기본
- Enum 활용
- Spring에서의 Enum활용(Message Converter)
- Spring에서의 Enum활용(Validation)
- MyBatis에서의 Enum활용(TypeHandler)
- 해당 사항 테스트 코드
Enum 설명
고정된 상수 집합이 필요할 때 Enum을 제일 많이 사용한다.
Java Enum은 Java 5부터 나왔다. Int Enum이나 String Enum을 대체하기 위해 나온 Java의 Enum기능은 강력하다.
보통 개발 할 때 아래와 같이 Int / String Enum을 많이 사용한다.
private static final int FRUIT_ORANGE = 1;
private static final int FRUIT_APPLE = 2;
private static final String OPERATION_MODULE_A = "op_a";
private static final String OPERATION_MODULE_B = "op_b";
private static final String OPERATION_MODULE_C = "op_c";
위와 같은 Enum들을 자주 봤을 것이다. 일단 얘기하자면 위와 같은 것은 Enum 안티 패턴이다.
아래와 같은 이유로 인해 Int/String Enum 패턴을 사용하면 프로그램이 쉽게 깨질 우려가 존재한다.
- 컴파일 시점의 상수 -> 상수의 유연한 핸들링이 힘들다.
- 그룹핑 기능이 없기 때문에 복잡한 기능을 소화하지 못 한다.
- Int Enum의 경우, 인쇄 가능한 문자열을 생성하기 피곤하다.
- String Enum의 경우, 문자열 비교를 하기 때문에 느리다.
- 타입 안전성을 보장하지 못 한다.
즉, 정리하자면 타입 세이프하지 못 하다. 때문에, 컴파일 타임에서 오류를 잡을 수 없다. 런타임 테스트를 통해 오류를 확인할 수 있다(int 또는 String형 만 받으면 다 되기 때문에 문제점을 찾기 위한 뎁스가 추가된다). 또한 확장성이 많이 부족하기 때문에 활용하는데 있어, 한계점이 존재할 수 밖에 없다(다만, 컴파일 시점 상수이기 때문에, 어노테이션에 쉽게 적용할 수 있다ㅎㅎ, Enum의 값은 어노테이션에 적용하지 못해 불편하다ㅠㅠㅠ).
그러나 우리는(나 포함) 간단하기 때문에 쓴닼ㅋㅋㅋㅋㅋㅋㅋ. 다만 제품화가 되는 프로그램은 Java Enum 을 쓰는 것을 강력하게 권고 하고싶다(또는 완성도를 더 높이고 싶은 토이 프로젝트).
Enum은 다음의 특징이 존재한다.
- Enum은 static final 하다.
- 계승(상속)이 불가능하다.
- 컴파일 타임, 타입 세이프하다.
- 그룹핑이 가능하다(Enum의 Enum을 하여 조합 가능).
- Object를 계상받아 Object에서 제공하는 메소드를 활용할 수 있다. 또는 디폴트 메소드를 사용할 수 있다.
- serializable, comparable이 가능하다.
- 메소드를 사용하여 기능 확장이 무궁무진하다(상수 + 관련 데이터의 연계 및 연산을 사용할 수 있다.).
- 비교 연산은 Int 상수와 성능이 비슷하다.
위와 같은 이유로 인해 코드 가독성이 좋으며, 형 안전성이 좋고, 기능이 강력하다.
Enum 기본
전형적으로 Enum을 사용하는 것은 다음과 같다.
public enum UserAuthority {
GREAT_USER( "USR_001" ),
GOOD_USER( "USR_002" ),
COMMON_USER( "USR_003" ),
BAD_USER( "USR_004" ),
GET_OUT_USER( "USR_005" );
private String authCode;
UserAuthority( String authCode ) {
this.authCode = authCode;
}
public String getCode() {
return this.authCode;
}
}
1. Enum은 DB의 코드 값이랑 많이 활용할 수 있다. 추가적으로 코드를 추가하는데도 무리가 없다.
2. 실질적으로 사용할 때 UserAuthority.GREAT_USER 이런식으로 타입 세이프하게 사용할 수 있다.
3. 접근 사용자가 권한의 가능 여부를 판단하는 기능을 메소드 추가를 통해 해소할 수 있다(상수 관련 연계 및 연산 처리).
4. Enum의 Enum을 사용하여, 권한의 권한으로 GREAT_USER, GOOD_USER에 해당하는 권한만 존재하는 Enum을 생성할 수 있다.
Enum 활용
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARTH (5.975e+24, 6.378e6),
MARS (6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN (5.685e+26, 6.027e7),
URANUS (8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
private final double mass; // In kilograms
private final double radius; // In meters
private final double surfaceGravity; // In m / s^2
// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;
// Constructor
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() { return mass; }
public double radius() { return radius; }
public double surfaceGravity() { return surfaceGravity; }
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}
해당 코드는 이펙티브 자바의 Enum 편에 제일 첫장에 나오는 소스 코드이다.
위와 같이 Enum을 활용할 수 있다.
- Planet.VENUS.mass() 또는 Planet.VENUS.radius() 를 통해 쉽게 상수 값을 확인할 수 있다.
- 코드를 보면 딱 이해할 수 있다(가독성 원탑).
- 해당 상수와 연계된 연산(surfaceWeight, surfaceGravity)을 해당 Enum에서 충분히 소화할 수 있다(SRP)
Enum은 EnumMap, EnumSet 활용하여 더 활용할 수 있으며, 인터페이스를 통해 유연하게 개발할 수 있다.
자세한 사항은 이펙티브 자바 Enum편 또는 Enum 활용에 대한 구글링을 참고한다. 정말 다양하게 Enum을 활용한다.
멋지다.
Spring/MyBatis에서 Enum을 사용할 때 골 때리는 구간이 좀 존재한다. 아래와 같이 해결한다.
Spring에서의 Enum활용(Message Converter)
스프링에서 Jackson Mapper를 통해 JSON 메시지에 대해 자동 Message Converting 기능을 사용할 때 Enum은 별도 처리가 필요하다. 아래와 같이 @JsonValue어노테이션을 지정하면 된다. 좀 아쉬운 점은 모든 Message Converter에 공통적으로 적용하기는 어려운 듯 하다.. 그래도 어노테이션 하나 지정을 통해 개선할 수 있어 다행이다.
@JsonValue
public String getCode() {
return this.authCode;
}
Spring에서의 Enum활용(Validation)
스프링에서는 Message Converting 작업과 동시에 Validation을 할 수 있게해준다. 그러면, Enum의 경우, 해당 작업을 어떻게 처리할까? @NotEmpty를 적용했을 경우 아래와 같은 오류를 뱉어 낸다.
Jackson mapper @NotEmpty 적용(실패)
{"status":"fail","errMsg":"UnexpectedTypeException: No validator could be found for type: **.EnumTestVo$UserAuthority","errCode":1}
별도의 처리를 하지 않을 경우, @NotNull @Null 만 적용할 수 있다. @NotNull을 처리할 경우, 아래와 같이 성공적인 벨리데이션 체크를 할 수 있다.
Jackson mapper @NotNull 적용(성공)
{"status":"fail","errMsg":"CustomIllegalArgumentException: Message Validation Error : [target: enumTestVo.userAuthority, errMsg: may not be null ] ","errCode":21013}
만약 NotEmpty와 그 이상의 Validation은 그냥 Custom Validation을 생성하면 된다.
EnumNamePattern.class
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = EnumNamePatternValidator.class)
public @interface EnumNamePattern {
String[] enumCodes();
String message() default "Enum value is invalid.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
EnumNamePatternValidator.class
public class EnumNamePatternValidator implements ConstraintValidator<EnumNamePattern, Enum<?>> {
private Pattern pattern;
@Override
public void initialize(EnumNamePattern annotation) {
try {
StringBuilder sb = new StringBuilder();
sb.append( annotation.enumCodes()[0] );
for( int i=1, len = annotation.enumCodes().length; i<len; i++ ) {
sb.append( "|" ).append( annotation.enumCodes()[i] );
}
pattern = Pattern.compile( sb.toString() );
} catch ( PatternSyntaxException e ) {
throw new IllegalArgumentException("Given regex is invalid", e);
}
}
@Override
public boolean isValid(Enum<?> value, ConstraintValidatorContext context) {
if (value == null) {
return false;
}
Matcher m = pattern.matcher(value.name());
return m.matches();
}
}
VO객체 일부 발췌.class
@EnumNamePattern(enumCodes = {"BAD_USER"})
private UserAuthority userAuthority;
간략하게 설명하자면 다음과 같다.
1. @EnumNamePattern(enumCodes={"BAD_USER"})의 경우, BAD_USER만 받는 VO 객체이다.
2. 해당 사항은 Custom Validation과 어노테이션을 추가한 것이다.
3. EnumNamePatternValidator에서 정규 표현식을 통해 참 / 거짓을 판단한다.
4. EnumNamePatternValidator에서 null 값에 대해 는 거짓으로 판단한다.
initialize 메소드에서 최대한 작업할 수 있는 것을 작업하자, isValid 에서 매 Request마다 작업을 진행하기 때문이다.
MyBatis에서의 Enum활용(TypeHandler)
이제 Controller 생성(아래 참고) 및 MyBatis를 연동한다. 우리는 "BAD_USER"가 아닌 DB 코드 값 "USR_004"를 입력하고 싶다. Typehandler를 적용하지 않을 경우, BAD_USER가 입력되는 불상사가 생긴다. MyBatis TypeHandler는 JDBC 프로그래밍에서의 ResultSet, PreparedStatement를 원하는대로 핸들링할 수 있다.
TypeHandler 미적용 감사로그(BAD_USER가 insert 된다)
[***]-2020-07-20 22:35:21.741 main DEBUG: **.mapper.enum.insertEnumTest - ==> Preparing: insert into enum_test( user_id, user_name, user_authority ) values ( ?, ?, ? );
[***]-2020-07-20 22:35:21.764 main DEBUG: **.insertEnumTest - ==> Parameters: ehdvudee(String), shin(String), BAD_USER(String)
[***]-2020-07-20 22:35:21.770 main DEBUG: **.enum.insertEnumTest - <== Updates: 1
[***]-2020-07-20 22:35:21.773 main DEBUG: **.mapper.enum.selectEnumTest - ==> Preparing: select user_id userId, user_name userName, user_authority userAuthority from enum_test
[***]-2020-07-20 22:35:21.773 main DEBUG: **.mapper.enum.selectEnumTest - ==> Parameters:
[***]-2020-07-20 22:35:21.800 main TRACE: **.mapper.enum.selectEnumTest - <== Columns: userid, username, userauthority
[***]-2020-07-20 22:35:21.800 main TRACE: **.mapper.enum.selectEnumTest - <== Row: ehdvudee, shin, BAD_USER
[***]-2020-07-20 22:35:21.803 main DEBUG: **.mapper.enum.selectEnumTest - <== Total: 1
VO객체 일부 발췌.class
@MappedTypes(UserAuthority.class)
public static class TypeHandler extends CodeEnumTypeHandler<UserAuthority> {
public TypeHandler() {
super( UserAuthority.class );
}
}
위의 코드와 같이 @MappedTypes를 지정한다. 솔직히 원리는 잘 모르겠다(원리에 대해 학습이 필요).
아래와 같이 TypeHandler 생성 및 MyBatis 설정에 TypeHandler를 추가한다.
CodeEnumTypeHandler.class
public abstract class CodeEnumTypeHandler<E extends Enum<E>> implements TypeHandler<EnumTestVo.CodeEnum> {
private Class<E> type;
public CodeEnumTypeHandler( Class <E> type ) {
this.type = type;
}
@Override
public void setParameter( PreparedStatement ps, int i, EnumTestVo.CodeEnum parameter, JdbcType jdbcType ) throws SQLException {
ps.setString( i, parameter.getCode() );
}
@Override
public EnumTestVo.CodeEnum getResult( ResultSet rs, String columnName ) throws SQLException {
String code = rs.getString( columnName );
return getCodeEnum( code );
}
@Override
public EnumTestVo.CodeEnum getResult( ResultSet rs, int columnIndex ) throws SQLException {
String code = rs.getString( columnIndex );
return getCodeEnum( code );
}
@Override
public EnumTestVo.CodeEnum getResult( CallableStatement cs, int columnIndex ) throws SQLException {
String code = cs.getString( columnIndex );
return getCodeEnum( code );
}
private EnumTestVo.CodeEnum getCodeEnum( String code ) {
try {
EnumTestVo.CodeEnum[] enumConstants = (EnumTestVo.CodeEnum[]) type.getEnumConstants();
for ( EnumTestVo.CodeEnum codeNum: enumConstants ) {
if ( codeNum.getCode().equals( code ) ) {
return codeNum;
}
}
return null;
} catch (Exception e) {
throw new TypeException("Can't make enum object '" + type + "'", e);
}
}
}
TypeHandler 적용(AppConfig.class)
private SqlSessionFactoryBean setCommonMybatisConfig( SqlSessionFactoryBean sessionFactory ) {
sessionFactory.setTypeAliases( new Class<?>[] {
EnumTestVo.class
});
sessionFactory.setTypeHandlers( new TypeHandler[] {
new EnumTestVo.UserAuthority.TypeHandler()
});
org.apache.ibatis.session.Configuration settings = new org.apache.ibatis.session.Configuration();
settings.setCacheEnabled( false );
settings.setJdbcTypeForNull( JdbcType.NULL );
sessionFactory.setConfiguration( settings );
return sessionFactory;
}
TypeHandler 적용 후("BAD_USER"가 아닌 "USR_004"가 들어가는 것을 확인할 수 있다.)
[***]-2020-07-20 22:40:01.195 main DEBUG: **.mapper.enum.insertEnumTest - ==> Preparing: insert into enum_test( user_id, user_name, user_authority ) values ( ?, ?, ? );
[***]-2020-07-20 22:40:01.236 main DEBUG: **.mapper.enum.insertEnumTest - ==> Parameters: ehdvudee(String), shin(String), USR_004(String)
[***]-2020-07-20 22:40:01.257 main DEBUG: **.mapper.enum.insertEnumTest - <== Updates: 1
[***]-2020-07-20 22:40:01.260 main DEBUG: **.mapper.enum.selectEnumTest - ==> Preparing: select user_id userId, user_name userName, user_authority userAuthority from enum_test
[***]-2020-07-20 22:40:01.261 main DEBUG: **.mapper.enum.selectEnumTest - ==> Parameters:
[***]-2020-07-20 22:40:01.291 main TRACE: **.mapper.enum.selectEnumTest - <== Columns: userid, username, userauthority
[***]-2020-07-20 22:40:01.292 main TRACE: **.mapper.enum.selectEnumTest - <== Row: ehdvudee, shin, USR_004
[***]-2020-07-20 22:40:01.294 main DEBUG: **.mapper.enum.selectEnumTest - <== Total: 1
최종 VO 객체
public class EnumTestVo {
@NotEmpty
private String userId;
@NotEmpty
private String userName;
@EnumNamePattern(enumCodes = {"BAD_USER"})
private UserAuthority userAuthority;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public UserAuthority getUserAuthority() {
return userAuthority;
}
public void setUserAuthority(UserAuthority userAuthority) {
this.userAuthority = userAuthority;
}
@Override
public String toString() {
return "EnumTestVo [userId=" + userId + ", userName=" + userName + ", userAuthority=" + userAuthority + "]";
}
public interface CodeEnum {
public String getCode();
}
public enum UserAuthority implements CodeEnum {
GREAT_USER( "USR_001" ),
GOOD_USER( "USR_002" ),
COMMON_USER( "USR_003" ),
BAD_USER( "USR_004" ),
GET_OUT_USER( "USR_005" );
private String authCode;
UserAuthority( String authCode ) {
this.authCode = authCode;
}
@Override
@JsonValue
public String getCode() {
return this.authCode;
}
@MappedTypes(UserAuthority.class)
public static class TypeHandler extends CodeEnumTypeHandler<UserAuthority> {
public TypeHandler() {
super( UserAuthority.class );
}
}
}
}
Spring/MyBatis 환경에서 Enum을 활용하기 위해서는 아래의 여건들이 충족되어야 한다.
- MessageConverter의 Enum 처리
- Validation 체크
- MyBatis에서 처리하기 위해 TypeHandler 지정
해당 사항 테스트 코드
해당사항을 테스트 하기 위해, Controller 생성 및 Spring 테스트 코드를 작성하였다.
HomeController.class
@RestController
@RequestMapping("/")
public class HomeControlller {
private final CommonErrorHandler commonErrorHandler;
private final SqlSession session;
@Autowired
public HomeControlller( CommonErrorHandler commonErrorHandler, SqlSession session ) {
this.commonErrorHandler = commonErrorHandler;
this.session = session;
}
@RequestMapping(value="user", method=RequestMethod.POST)
public ResponseEntity<?> createUser( @RequestBody @Valid EnumTestVo enumTestVo, BindingResult bindingResult, WebRequest request ) {
if ( bindingResult.hasErrors() ) {
String errMsg = commonErrorHandler.getValidationErrMsg( bindingResult );
throw new CustomIllegalArgumentException( ErrorCode.INVALID_HTTP_BODY, errMsg );
}
System.out.println( enumTestVo );
// SERVICE & DAO AREA
session.insert("**.mapper.enum.insertEnumTest", enumTestVo );
EnumTestVo returnVo = session.selectOne( "**.mapper.enum.selectEnumTest", enumTestVo );
// SERVICE & DAO AREA
return ResponseEntity.ok( returnVo );
}
}
EnumTest.class
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes= {WebConfig.class, AppConfig.class})
@Transactional
public class EnumTest {
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
private ObjectMapper objectMapper;
@Before
public void init() {
this.mockMvc = MockMvcBuilders.webAppContextSetup( context ).build();
this.objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion( JsonInclude.Include.NON_NULL );
objectMapper.setDateFormat( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss") );
}
@Test
public void run() throws Exception {
// GIVEN
EnumTestVo vo = new EnumTestVo();
vo.setUserId( "ehdvudee" );
vo.setUserName( "shin" );
vo.setUserAuthority( EnumTestVo.UserAuthority.BAD_USER );
String jsonData = objectMapper.writeValueAsString( vo );
System.out.println( "[Request JSON Body : " + jsonData + " ]");
// WHEN THEN
MvcResult result = mockMvc.perform( post( "/user" )
.header( HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE )
.header( HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE )
.header( HttpHeaders.CONTENT_ENCODING, "UTF-8" )
.content( jsonData )
)
.andDo( print() )
.andExpect( status().isOk() )
.andReturn();
System.out.println( "[Response Body : " + result.getResponse().getContentAsString() + " ]");
}
}
최종 로그
[Request JSON Body : {"userId":"ehdvudee","userName":"shin","userAuthority":"USR_004"} ]
[***]-2020-07-20 22:40:01.195 main DEBUG: **.mapper.enum.insertEnumTest - ==> Preparing: insert into enum_test( user_id, user_name, user_authority ) values ( ?, ?, ? );
[***]-2020-07-20 22:40:01.236 main DEBUG: **.mapper.enum.insertEnumTest - ==> Parameters: ehdvudee(String), shin(String), USR_004(String)
[***]-2020-07-20 22:40:01.257 main DEBUG: **.mapper.enum.insertEnumTest - <== Updates: 1
[***]-2020-07-20 22:40:01.260 main DEBUG: **.mapper.enum.selectEnumTest - ==> Preparing: select user_id userId, user_name userName, user_authority userAuthority from enum_test
[***]-2020-07-20 22:40:01.261 main DEBUG: **.mapper.enum.selectEnumTest - ==> Parameters:
[***]-2020-07-20 22:40:01.291 main TRACE: **.mapper.enum.selectEnumTest - <== Columns: userid, username, userauthority
[***]-2020-07-20 22:40:01.292 main TRACE: **.mapper.enum.selectEnumTest - <== Row: ehdvudee, shin, USR_004
[***]-2020-07-20 22:40:01.294 main DEBUG: **.mapper.enum.selectEnumTest - <== Total: 1
MockHttpServletRequest:
HTTP Method = POST
Request URI = /user
Parameters = {}
Headers = {Content-Type=[application/json], Accept=[application/json], Content-Encoding=[UTF-8]}
Handler:
Type = **.HomeControlller
Method = public org.springframework.http.ResponseEntity<?> **.HomeControlller.createUser(**.EnumTestVo,org.springframework.validation.BindingResult,org.springframework.web.context.request.WebRequest)
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
MockHttpServletResponse:
Status = 200
Error message = null
Headers = {Content-Type=[application/json;charset=UTF-8]}
Content type = application/json;charset=UTF-8
Body = {"userId":"ehdvudee","userName":"shin","userAuthority":"USR_004"}
Forwarded URL = null
Redirected URL = null
Cookies = []
[Response Body : {"userId":"ehdvudee","userName":"shin","userAuthority":"USR_004"} ]
참고
https://www.holaxprogramming.com/2015/11/12/spring-boot-mybatis-typehandler/
https://www.baeldung.com/javax-validations-enums
https://woowabros.github.io/tools/2017/07/10/java-enum-uses.html
https://androphil.tistory.com/707
'개발관련 > 삽질' 카테고리의 다른 글
LoRa 1.0 취약점 분석 (0) | 2021.02.04 |
---|---|
LoRaWAN 1.1 보안 부분 분석(LoRa Spec 6장 일부분 번역) (0) | 2021.02.04 |
샤미르의 비밀 공유(SSS, Shamir's Secret Sharing) - 구현 (0) | 2020.05.12 |
Python TensorFlow를 이용한 몸무게 예측 프로그램 (0) | 2020.05.07 |
샤미르의 비밀 공유(SSS, Shamir's Secret Sharing) - 이론 (6) | 2020.05.07 |