이직한 곳에서의 주 백엔드 개발 언어가 node.js 에 nest.js를 사용하는 환경이라 좀 생소하다. 나에게 맞지 않는 옷을 입은 느낌이 자꾸 든다. 다양한 경험을 위해 이직한 만큼 이곳에서 또 충분히 성장한 후 언젠가 다시 java로 돌아갈 때가 올거라고 믿기에 자바를 계속 갈고 닦기 위해 개인 프로젝트를 자바로 진행하려 한다. 화이팅~
enum을 사용하는 이유는 여러 가지가 있으나, 가장 큰 이유는 DB의 코드값과 달리 IDE 또는 컴파일 단계에서 지원을 받을수 있다는게 가장 큰 이유일 것이다. 자동완성도 지원하며 매직넘버와 달리 오타를 냈다면 컴파일 단계에서 잡아준다. 그말이 그말이잖..
장점, 장점을 보자
컴파일 단계에서 지원을 받을 수 있다
IDE에서는 자동완성도 되고, 오타가 났을 경우에 런타임 에러가 아닌 컴파일 에러를 내므로 버그를 만들 확률이 줄어든다.(어디까지나 완벽히 없어지는건 아니다. 버그 장인은 도구를 탓하지 않는다 )
리팩토링 또는 수정이 쉽다
값이 추가 되더라도 enum값 하나만 추가 하면 된다. 혹시 enum이 내포하고 있는 값이 달라진다면 enum만 수정하면 된다.
그 외에도
기본적으로 enum은 선언시 불변객체로 생성되므로 여러번 할당하더라도 새로 object를 생성하는 비용 부담이 덜하고, String과는 달리 equals를 사용하여 비교할 필요가 없다. == 를 사용하여 비교해도 잘 작동한다.
그렇다면 단점은?
배포 문제에서 자유롭지 못하다
컴파일 단계에서 작동해서 IDE의 지원을 받을 수 있다는 건, 반대로 배포 문제에서 자유롭지 못하다는 뜻이기도 하다. 간단한 값 추가가 있다고 하더라도 DB에서 사용하는 코드와는 달리 배포를 해야 한다. 요즘은 CI/CD가 워낙 잘 돼있는데다가 무중단 배포 환경도 손쉽게 구축하여 사용하는 추세라 크게 문제가 안된다고 할 수 있지만, 그렇다고 하더라도 분명 DB에서 손쉽게 추가하는 것에 비하면 부담이 되는 부분은 사실이다.
만약 DB에서 일원화된 persistant 영역에서 관리하는 코드값이라고 하면 코드만 추가 해주면 끝이지만, 여러 서비스에서 사용하는 값이라면 각 서비스 별로 챙겨서 배포해야 하며, 그러다 보면 누락하는 서비스가 있을수도 있다. (그게 우리 서비스도 이랬던 적이 있더랬..) 또한 이런식으로 누락하는 서비스에서 발생하는 오류는 단순히 DB 코드를 추가했다가 생기는 오류보다 더 크리티컬 하다.
enum이 적극적으로 쓰이지 않던 때에는 거의 모든 상태값들을 코드 테이블을 하나 만들어놓고 다 때려 박았다. 사실 배포 문제에서 자유롭지 못한 문제가 있지만, 생각보다 우리가 코드라고 생각하는 아이들은 사실은 코드가 아닌 경우가 많다. 자주 바뀌는 값과 바뀌지 않는 값을 잘 구분해서 자주 바뀌지 않는 녀석들을 enum으로 선언해서 사용하자.
숫자만 넣어서 만들수는 없다
망치를 든 사람에게는 모든게 못으로 보인다고, enum의 맛을 알게 되면 모든 코드를 enum으로 바꾸고 싶은 욕망에 사로잡히게 된다. 그러나 이렇게 enum으로 하나둘씩 바꾸다 보면 숫자로만 돼있는 코드들은 enum으로 만들 수 없다는 것을 깨닿게 된다. enum의 상태값들은 java의 필드명 정의하는 규칙과 같이 숫자로만 구성할 수는 없다.
사실 대부분 enum 상태값으로 정의하고자 하는 속성은 숫자가 아닌 다른 뜻을 갖고 있는 경우가 많이 있지만 진짜 숫자만 있는 경우라면? 대체할 만한 단어가 없다면??? 그럼 그냥 다른 문자를 붙이는 수밖에 없다.
1 2 3
enumNumbers{ _1, _2, _3; }
별로 맘에는 안든다. 그렇지만 범위를 제한할 수 없는 int 필드보다는 낫다.
1 2 3
enumNumbers{ 1ST, 2ND, 3RD; }
뭐 이런것도 나쁘지는 않다.
좀 더 고급지게 사용해보자
자바의 enum은 다른 언어의 enum에 비해서 좀 더 활용도가 높다. 사실 java에서 enum은 하나의 완전한 클래스로 취급 되는데 이 때문에 enum도 인스터스 변수를 가질수도 있고, interface를 implements 하도록 하거나 abstract로 선언할 수도 있으며 method나 static method를 만들수도 있다.
아래 예제를 보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
enumSequences{ FIRST(1), SECOND(2), THIRD(3);
privateint order;
Sequences(int order) { this.order = order; }
publicintgetOrder(){ return order; } }
이렇게 단순하게 사용하는 경우는 별로 없지만(심지어 java는 ordinal() 이라는 기본 메소드를 제공한다. 그러나 ordinal은 가급적 사용하지 않는것이 좋다. 이유는 후술) 코드에서 보이는 바와 같이 클래스의 모습과 크게 다르지 않다. 더욱이 자바의 enum은 이런 변수를 하나만 사용 가능한 것이 아니라 여러개가 사용 가능하다.
참고로 FIRST(1, "1st") 이렇게 enum을 선언하는 구문은 생성자 메소드를 호출하는 방식이기 때문에 생성자를 어떻게 만드느냐에 따라 다른 방식으로도 사용 가능하다. 또한 enum은 static 영역에 저장되기 때문에 enum을 호출할때마다 생성자를 호출하는것이 아니어서 알고리즘의 효율성을 그렇게까지 신경쓰지 않아도 된다.
또한 enum에서 변수로 익명함수를 담고 있을 수도 있기 때문에 이를 사용하면 더욱더 활용도가 높아진다. 간단한 사칙연산을 수행하는 enum이다. (Lombok도 잘 먹혀서, 아래부터는 lombok을 활용해 constructor를 만들어 보겠다.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
@AllArgsConstructor enumCalc{ ADD("+", (a, b) -> a + b), SUBTRACT("-", (a, b) -> a - b), TIMES("*", (a, b) -> a * b), MOD("/", (a, b) -> a / b); @Getter private String character;
private BiFunction<Integer, Integer, Integer> calculation; publicintcalc(int a, int b){ return calculation.apply(a, b); } }
아래는 위와 같은 일을 하는 코드를 abstract 메소드를 활용하여 만들어 본 코드이다. (java7) 이렇게도 사용 가능하니 적절히 필요에 따라 원하는 방법대로 사용하면 좋다.
@Getter @AllArgsConstructor publicenumCalc{ ADD("+"){ @Override publicintcalc(int a, int b){ return a + b; } }, SUBTRACT("−"){ @Override publicintcalc(int a, int b){ return a - b; } }, TIMES("×"){ @Override publicintcalc(int a, int b){ return a * b; } }, MOD("÷"){ @Override publicintcalc(int a, int b){ return a / b; } };
private String character;
publicabstractintcalc(int a, int b); }
이런식으로 enum에 디자인 패턴을 얹어 사용하면 코드들이 단순 명료해 지는것을 느낄 수 있을 것이다.
왜 ordinal()을 사용하지 않아야 하나?
아까 잠깐 언급하고 넘어간 ordinal() 메소드에 대해 이야기 해보자. enum은 순서를 가진 열거형이고, 그 순서값을 int로 반환하는 ordinal()이라는 메소드를 제공해준다. 그러나 ordinal을 사용하게 되면 나중에 유지보수에 문제가 발생하게 된다. (이 부분은 effective java item 35를 보면 자세히 나온다)
중간에 새로운 enum 요소가 끼어 드는 경우 새로운 요소 뒤에 위치한 enum들의 연산은 모두 망가지게 된다.
따라서 ordinal을 사용하는 것보다 차라리 위에서 만든 Sequence enum처럼 인스턴스 필드를 사용하여 int value를 담아두고 이를 return하는 getter를 사용하는것이 더 좋다.
내가 java 에서 enum을 사용하는 방법은 이정도이다. 물론 더 다양 하게 사용하는 방법도 있다. (enum 내부에 또 enum을 정의해 사용한다던지, 좀 다른 이야기지만 EnumMap을 사용하는 방법도 있고) 그러나 이정도만 알아도 java enum 초보 티는 좀 벗었다고 할 수 있지 않을까?
사실 이 이야기는 Effective Java 6장 - 불필요한 객체 생성을 피하라 에 나오는 아주 기초적인 이야기 이다. 그러나 습관이 되지 않아 생각없이 개발하다보면 어느새 이 규칙을 어겨서 어김없이 리팩토링 대상이 되곤 하는데다가, 막상 개발하면서 쓰려다 보면 사용 방법이 잘 생각나지 않아 메모할 겸 짧은 글을 작성 해본다. (요즘 좀 정신이 나가 있어서 공부는 꾸준히 하기는 한다만, 공부하는 양은 좀 줄었고, 진도는 좀처럼 나가지가 않는다. 에라이)
@DisplayName("Utils 테스트") classUtilsTest{ @Test @DisplayName("숫자 외의 문자가 있을 경우 false를 return 해야 한다") voidshould_return_false_when_has_other_characters(){ String numbersWithCharacter = "1a2s3d4f5g6"; boolean expected = false;
boolean result = Utils.hasOnlyNumbers(numbersWithCharacter);
assertEquals(expected, result); } @Test @DisplayName("숫자외의 문자가 없을 경우 true를 return 해야 한다") voidshould_return_true_when_has_only_numbers(){ String numbersWithCharacter = "123456789"; boolean expected = true;
boolean result = Utils.hasOnlyNumbers(numbersWithCharacter);
assertEquals(expected, result); } }
오케이, 원하는 바를 손쉽게 얻었다. 그러나 지금 얻은 결과는 성능상 좋지 못하다. 사실 일반적인 상황에서 String의 matches 메소드를 사용하는게 엄청나게 성능에 문제를 주는 경우는 별로 없으나, 습관이 중요한데다가, 실제로 이런 코드들이 쌓여 문제를 발생하는 경우도 있으니 좋은 습관을 들이는 게 좋다. 이펙티브 자바는 String.matches를 사용하는 것보다 더 고급지고어렵고 성능이 좋은 방법을 알고 있다.
Pattern 클래스를 사용해 보자
바로 Pattern클래스를 사용해 같은 일을 더 성능 좋게 사용할 수 있다. 사실 위 예제의 코드는 메소드가 실행 될때마다 내부적으로 Pattern 인스턴스를 만들고 사용 후 버리는 작업을 수행하게 되는데, 이 Pattern을 final static 객체로 미리 선언해두고 사용하게 되면 메소드가 호출될 때마다 새로 생성하고 버리는 작업이 없어지게 된다. 아래 코드를 보자
Spring boot 에서는 application.yml (또는 application.properties) 파일의 내용을 빈의 변수에 바인딩하도록 도와주는 어노테이션이 있다. @Value("${property.value}") 뭐 이런식으로 사용하는데 변수가 늘어남에 따라 어노테이션이 많아져서 가독성을 해치는 경우가 있고, 일일히 지정해주기가 귀찮을 경우 쓸 수 있는 방법인 @ConfigurationProperties 어노테이션에 대해서 간단히 적어 보려 한다.
사용방법
@ConfigurationProperties 는 클래스에 선언하는 어노테이션이다. 간단히 아래 예시와 같이 사용한다.
값들을 사용하고자 하는 클래스에 @ConfigurationProperties 어노테이션을 추가한다. 여기서 prefix도 설정할 수 있다. (user.conf는 예시를 위해 작성해 놓은 것이고, prefix는 다른 값으로도 사용할 수 있다.) 주의할 점은 @Value 어노테이션을 활용해 값을 바인딩 하는것과 다르게, 반드시 setter 메소드를 필요로 한다. @ConfigurationProperties는 setter 메소드를 사용해 설정값을 바인딩 하기 때문이다. 여기서는 클래스에 @Setter 롬복 어노테이션을 사용해 한번에 정의했다.
Amazon MQ 선택시 마지막까지 고민하게 만들었던 부분은 spring-cloud-stream을 사용하여 구현할 수 없다는 점이었는데요, Spring-cloud-stream 을 사용하여 서비스를 구현하면 간단한 어노테이션 추가만으로도 구현이 가능했던 점이 아주 큰 장점이었거든요.
But, 찾아보니 Amazon MQ도 비슷한 난이도로 구현할 수 있는 방법이 있어 이를 구현하는 방법을 정리 차원에서 작성하고자 합니다. start!
Amazon console에서 Active MQ를 설정하는 방법은 건너뛰도록 하겠습니다.
spring boot을 사용하여 Project를 생성
저는 spring-boot-devtools 정도만 추가 했습니다.(필요에 따라서는 lombok등도 추가 해주시면 됩니다..)
//given Message msg = new Message(); msg.setId("1"); msg.setName("a message from JH");
//when producer.send(msg);
//then Thread.sleep(1000L); assertThat(outputCapture.toString().contains("1, a message from JH")).isTrue(); }
}
테스트 클래스를 실행해봅시다. 실행 전에 실행 환경 변수에 SERIALIZABLE_PACKAGES 설정값을 추가 해 주어야 오류 메세지를 만나지 않고 pojo를 전송하고 받을 수 있게 됩니다. 보안 상 신뢰할 수 있는 패키지를 환경설정값에 넣어야만 하도록 되어있기 때문입니다.