우아한 형제들 기술블로그의 enum과 관련된 글을 예전에 읽었는데 이번에 적용할 수 있는 기회가 생겨서 적용해봤습니다. (상세한 활용은 링크를 통해서 학습하면 좋을 것 같습니다.)
Enum 어디에 적용했는가?
평소에는 static final 대신 상수로 적용해야할 값들이 있을 때 주로 사용했었다.
그런데 이번에는 똑같이 상수지만, 조금 특별하게 사용했다.
상황을 설명하면, 어떤 PUBSUB의 메세지 처리 플랫폼(ex. kafka)을 이용해야하는데, 거기에 publish할 때 쓰는 메세지의 타입이 3가지가 있고, 그 3가지중에 하나를 설정 파일에서 정의하면, 그 설정 기준으로 publish하기 전에 방식을 적용해야하는 상황이다. (결론 -> 설정 파일에 쓰인 값 적용하기)
pubsub:
messageType: non_persistents # [persistent, non_persistent, direct]
일단 custom configuration으로 application.yml파일에 위와 같이 정의했다. (3가지 메세지 타입은 persistent, non_persistent, direct로 해당 플랫폼에서 쓰는 값들이다.)
그 다음에 당돌하게(?) @Value로 yml파일의 속성 값을 String으로 가져와서 구분한 뒤 적용하는 식의 스타일을 썼다.
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.configuration.Type;
import com.example.demo.product.MessageSetting;
@RestController
public class TestController {
@Value("${pubsub.messageType:DIRECT}")
private String oldMessageType;
@GetMapping("/old")
public void oldPublish(MessageSetting messageSetting) {
//settings
if("persistent".equalsIgnoreCase(oldMessageType)) {
messageSetting.setType(Type.PERSISTENT);
}else if("non_persistent".equalsIgnoreCase(oldMessageType)) {
messageSetting.setType(Type.NON_PERSISTENT);
}else if("direct".equalsIgnoreCase(oldMessageType)) {
messageSetting.setType(Type.DIRECT);
}else {
messageSetting.setType(Type.DIRECT);
}
//publish
//...
System.err.println(messageSetting.getType());
}
}
편의상(?) 위와 같이 작성했다.
문제점이 아주 많다.
- String으로 받기 때문에 입력 값을 예측할 수가 없다. 따라서 유효한지 검증하는 코드가 들어가야한다. 위에서는 @Value의 default 값으로 :DIRECT라고 했기 때문에 yml파일에 속성이 없으면 DIRECT가 될 것이다. 또한 예상치 못한 문자열이 들어오더라도 else문에 의해서 DIRECT로 지정될 것이다. 하지만 이렇게 불필요하게 방어적인 코드가 들어가야하는 문제가 있다.
- 지금은 3가지 타입인데 플랫폼에서 1가지를 더 추가하면 또 다시 if-else 구문을 추가해야한다. 또한 새로운 사람이 이어서 개발해야할 때 연관이 되어있는지를 잘 모를 수 있다.
- 해당 프로퍼티가 String으로 되어있어 직관적으로 어디에 쓰이는지 모를 수 있고, 어떤 타입인지를 판단하는 부분과 타입에 따른 행위가 나눠져 있는 문제가 있다.
이 문제를 해결하기위해 Enum을 적용했다. (Enum의 경우 어찌되었든 상수 형태이기 때문에 모든 경우에 적용하기는 어렵다..)
package com.example.demo.configuration;
import java.util.function.Consumer;
import com.example.demo.product.MessageSetting;
public enum MessageType {
PERSISTENT(messageSetting->messageSetting.setType(Type.PERSISTENT)),
NON_PERSISTENT(messageSetting->messageSetting.setType(Type.NON_PERSISTENT)),
DIRECT(messageSetting->messageSetting.setType(Type.DIRECT));
private Consumer<MessageSetting> expression;
private MessageType(Consumer<MessageSetting> expression) {
this.expression = expression;
}
public void setMessageSetting(MessageSetting messageSetting) {
expression.accept(messageSetting);
}
}
여기서는 MessageSetting이라는 임의의 클래스를 만들었지만 실무에서는 해당 플랫폼 라이브러리에 정의된 것을 사용했다. (느낌만 보면 된다)
enum으로 3가지 타입을 정의했다.(PERSISTENT, NON_PERSISTENT, DIRECT)
enum이 온전한 클래스라는 것을 이용해서 내부 속성으로 Cousumer 인터페이스를 정의했다.
이 속성을 생성자에서 설정하도록 했고, 생성자 요구사항에 맞게 람다식을 이용해서 만들었다.
그래서 해당 Enum 객체마다 자신만의 설정 방법을 갖게 되었다.
여기에다가 해당 속성을 사용하기 위한 메서드를 만들었다.(setMessageSetting())
참고한 블로그에서는 Funtion인터페이스를 사용했었다. 왜냐하면 거기서는 파라미터가 있고, 리턴 값도 필요했기 때문이다. 내가 Consumer를 사용한 이유는 return값이 필요없기 때문이었다.
따라서 Funtion, Consumer, Supplier, Operator, Predicate 등을 활용하면 더 다양하게 작성할 수 있을 것이다.
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.configuration.MessageType;
import com.example.demo.product.MessageSetting;
@RestController
public class TestController {
@Value("${pubsub.messageType:DIRECT}")
private MessageType newMessageType;
@GetMapping("/new")
public void newPublish(MessageSetting messageSetting) {
//settings
newMessageType.setMessageSetting(messageSetting);
//publish
//...
System.err.println(messageSetting.getType());
}
}
메서드가 사실상 한줄로 변경되었다.
yml파일에 있는 property값을 enum으로 mapping하는 방법은 그냥 string으로(ex. DIRECT) 설정하면 저절로 mapping된다.
여기까지만해도 enum 적용은 끝났다. yml파일에 pubsub.messageType의 값은 대소문자는 구분없이 잘 매핑한다.
direct로 써도 DIRECT로 써준다. 아까 말했듯 @Value에 :DRIECT로 default값도 설정했기 때문에 yml파일에 pubsub.messageType 이라는 값 자체가 없어도 문제없이 애플리케이션이 실행되고 DIRECT 방식으로 지정된다.
심지어 [DIRECT, PERSISTENT, NON_PERSISTENT] 이 셋 중에 하나의 값이 아닌 이상한 값(ex. JDK)가 들어가면 바인딩(매핑)에 실패했다고 애플리케이션이 실행되지도 않는다.
꽤나 방어를 마친듯 하다... 만, 주제와는 조금 벗어나지만 더 정리할 것이 있다.
@Value annotation은 사용을 지양해야한다. 여러 이유가 있겠지만 여러 곳에서 Value로 가져다 쓰다보면 어떤 클래스에서 어떤 값을 가져다 쓰는지 알기 어렵기 때문이다.
그리고 이외에 방어적인 코드를 짤 수 있도록 하려면 @ConfigurationProperties를 사용해야한다.
그래서 아래와 같이 사용하는 것으로 마무리 지었다.
package com.example.demo.configuration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix="pubsub")
public class MessageConfiguration {
private MessageType messageType;
public MessageType getMessageType() {
return messageType;
}
public void setMessageType(MessageType messageType) {
this.messageType = messageType;
}
public MessageConfiguration() {
this.messageType = MessageType.DIRECT;
}
}
[ConfigurationProperties로 yml에서 설정값을 가져오는 빈 생성]
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.configuration.MessageConfiguration;
import com.example.demo.configuration.MessageType;
import com.example.demo.product.MessageSetting;
@RestController
public class TestController {
@Autowired
private MessageConfiguration messageConfiguration;
@GetMapping("/new2")
public void new2Publish(MessageSetting messageSetting) {
MessageType messageType = messageConfiguration.getMessageType();
messageType.setMessageSetting(messageSetting);
System.err.println(messageSetting.getType());
}
}
Configuration을 주입받아서 사용했다!
참고 사이트
http://woowabros.github.io/tools/2017/07/10/java-enum-uses.html