Java Enum 다형성으로 Notification 처리 리팩토링하기 - 조건문 없는 전략 설계

2025. 7. 26. 16:10·Java & Spring

 

이번 포스팅에서는 Java 의 Enum을 통해 분기처리 없이 알림 타입에 따른 전송을 구현해 보고자 합니다.

 

실제 프로젝트를 진행하면서 Email, Slack, SMS 등 상황에 따라 알림을 다른 플랫폼으로 전송해야 될 일이 있었습니다. 기존에는 그런 상황에서 if ... else 구문으로 보내야되는 알림 타입에 대해 처리했고 만약 알림 타입이 추가된 경우엔 또 다시 else if 를 추가하는 등의 작업이 필요했습니다.

 

하지만, 그렇게 하다보니 코드도 너무 복잡해지고 실수로 작업해야되는 알림 타입에 다른 로직을 추가 하는 경우도 발견했습니다.

 

그래서 이를 해결하기 위한 방법을 찾던 중 Java 의 Enum 을 활용하면 해결을 할 수 있다는 힌트를 얻을 수 있었고 실제 적용을 해보니 코드도 정말 깔끔해지고 추후에 타입을 추가할 때 너무 편하게 할 수 있었습니다.

 

그럼 if 문부터 시작해서 enum 까지 코드를 확인해보며 어떻게 코드가 정리되는지 정리해 보고자 합니다.

 

만약 다음과 같은 요구사항이 있다 가정해 보겠습니다.

1. 요청은 하나의 엔드포인트에서 보내야 됩니다.
2. 알림 타입에 따라 서로 다른 알림 플랫폼을 통해 전달되어야 합니다.
3. 알림 타입별 전송해야 되는 email 또는 연락처, id 의 포맷이 맞는지 확인이 필요합니다.
4. 알림 타입은 추후에 추가될 수 있고 비지니스 로직은 변경될 수 있습니다.

1. if .. else 를 통한 알림 전송

1. 다음과 같이 type, target, message 를 담은 요청을 받는 객체가 있습니다.

@Getter
@Builder
public class NotificationRequest1 {

    private final String type;
    private final String target;
    private final String message;
}

 

2. 요청이 넘어오면 다음과 같이 각 요청의 타입에 따라 분기 처리가 필요합니다. 

만약 새로운 타입의 알림 타입이 추가된다면, else if 구문을 추가로 구성하고 해당 알림에 대한 로직을 작성해야 될 것입니다.

 

그리고 알림 타입별로 유효성 검사를 위한 메소드도 계속 추가가 필요합니다.

public String send1(NotificationRequest1 notificationRequest1) throws BadRequestException {
        String type = notificationRequest1.getType();
        String message = notificationRequest1.getMessage();
        String target = notificationRequest1.getTarget();
        if (type.equals("SMS")) {
            isPhoneNumber(target);
            log.info("SMS 전송을 하였습니다. 대상자 : {} , message : {}", target, message);
            return "SMS";
        } else if (type.equals("EMAIL")) {
            isEmailFormat(target);
            log.info("EMAIL 전송을 하였습니다. 대상자 : {} , message : {}", target, message);
            return "EMAIL";
        } else if (type.equals("PUSH")) {
            isPushId(target);
            log.info("EMAIL 전송을 하였습니다. 대상자 : {} , message : {}", target, message);
            return "PUSH";
        } else{
            throw new BadRequestException("잘못된 타입의 알림 요청하였습니다. 요청 타입 = " + type);
        }
}

public void isPhoneNumber(String target){
    log.info("phone number target check");
}

public void isEmailFormat(String target){
    log.info("email target check");
}

public void isPushId(String target){
    log.info("push id target check");
}

 

위 코드를 지속적으로 사용한다고 하면 코드는 점점 복잡해지고 유지보수도 점점 어려워 질게 보입니다.

 

3. 테스트 코드를 통한 동작 확인

 

@Test
@DisplayName("이메일 전송을 보낼 수 있다. (if-else)")
void sendEmail1() throws BadRequestException {
    NotificationRequest1 notificationRequest1 =
            NotificationRequest1.builder().type("EMAIL").message("이메일 보냅니다")
                    .target("teest@gmail.com").build();
    String sendType = notificationService.send1(notificationRequest1);
    assertThat(sendType).isEqualTo("EMAIL");
}

 

그러면 Enum 을 통한 코드를 확인해 보겠습니다.

 

2. Enum 을 통한 알림 전송

 

1. 서비스 코드는 다음과 같이 분기구문이 다 사라지고 오직 유효성 체크 및 전송 로직만 존재합니다.

public String send2(NotificationRequest1 request) throws BadRequestException {
    NotificationType notificationType;
    try {
        notificationType = NotificationType.valueOf(request.getType().toUpperCase());
    } catch (IllegalArgumentException e) {
        throw new BadRequestException("지원하지 않는 알림 타입입니다: " + request.getType());
    }
    String target = request.getTarget();
    String message = request.getMessage();
    notificationType.validateTarget(target);
    return notificationType.send(target, message);
}

 

2. Enum 에서 실제 전송 및 유효성 체크에 대한 로직들을 다 담고 있고 Service 에서는 다음 Enum 의 코드들만 호출하게 되므로 가독성이 올라가고 추가 되는 타입에 대해서는 Enum 타입 추가만으로 해결할 수 있게 됩니다.

@Slf4j
public enum NotificationType {
    SMS {
        @Override
        public String send(String target, String message) {
            log.info("SMS 전송을 하였습니다. 대상자 : {} , message : {}", target, message);
            return "SMS";
        }
        @Override
        public void validateTarget(String target) {
            log.info("phone number target check");
        }
    },

    EMAIL {
        @Override
        public String send(String target, String message) {
            log.info("EMAIL 전송을 하였습니다. 대상자 : {} , message : {}", target, message);
            return "EMAIL";
        }
        @Override
        public void validateTarget(String target) {
            log.info("EMAIL number target check");
        }
    },

    PUSH {
        @Override
        public String send(String target, String message) {
            log.info("EMAIL 전송을 하였습니다. 대상자 : {} , message : {}", target, message);
            return "PUSH";
        }
        @Override
        public void validateTarget(String target) {
            log.info("push id target check");
        }
    };

    public abstract String send(String target, String message);
    public abstract void validateTarget(String target);
}

 

확실히 기존 if .. else 구문보다 더 코드가 분리되고 가독성도 높아진 것을 확인할 수 있습니다.

 

3. 테스트 코드를 통한 동작 확인

@Test
@DisplayName("이메일 전송을 보낼 수 있다.(enum)")
void sendEmail2() throws BadRequestException {
    NotificationRequest1 notificationRequest1 =
            NotificationRequest1.builder().type("EMAIL").message("이메일 보냅니다")
                    .target("teest@gmail.com").build();
    String sendType = notificationService.send2(notificationRequest1);
    assertThat(sendType).isEqualTo("EMAIL");
}

 

여기까지만 해도 충분하지만, 좀 더 나아가 enum 을 서비스에서 체크하는게 아닌 Controller 요청 단계에서 Validation 체크한 후 Service 에서는 단순 enum 을 사용하는 로직으로 변경해 보겠습니다.

 

3. Request 객체에 Enum 을 통한 구성

 

1. type 을 String 이 아닌 enum 타입인 NotificationType 으로 정의합니다.

@Getter
@Builder
public class NotificationRequest2 {

    private final NotificationType type;
    private final String target;
    private final String message;
}

 

2. Controller 에서는 Validation 을 통해 객체의 유효성 검사를 진행합니다.

@RestController
@RequiredArgsConstructor
@RequestMapping("/notification")
public class NotificationController {

    private final NotificationService notificationService;

    @PostMapping("")
    public String send(@RequestBody @Valid NotificationRequest2 request2){
        return notificationService.send3(request2);
    }
}

 

이렇게 준비가 된 후에 enum 에 잘못된 타입의 String 을 넘겼을 때 어떻게 되는지 확인해 보도록 하겠습니다.

 

3. Controller Test 를 통한 잘못된 enum 타입의 요청

@Test
@DisplayName("enum 타입이 잘못되어 요청에 실패한다.")
void create() throws Exception {
    String invalidJson = """
        {
            "type": "KAKAOTALK",
            "target": "01012345678",
            "message": "테스트 메시지"
        }
        """;
    mockMvc.perform(
            post("/notification")
                    .content(invalidJson)
                    .contentType(MediaType.APPLICATION_JSON)
    ).andExpect(status().isBadRequest()); //응답 상태가 400인지 확인한다!
}

 

 

 

"KAKAOTALK" 타입은 존재하지 않아 400 에러가 발생하여 실패되는 것을 확인할 수 있습니다.

 

그러면 이제 제대로된 Enum 타입이 넘어가 Service 에서 수행되는 로직을 확인해 보도록 하겠습니다.

 

4. Request 객체에 담긴 Enum 활용을 통한 Service

public String send3(NotificationRequest2 request) {
    NotificationType notificationType = request.getType();
    String target = request.getTarget();
    String message = request.getMessage();
    notificationType.validateTarget(target);
    return notificationType.send(target, message);
}

 

서비스 코드는 더 이상 enum 타입에 대한 체크를 할 필요 없고 바로 유효성 체크 및 알림 전송을 처리할 수 있게 되었습니다.

 

5. 테스트 코드를 통한 동작 확인

@Test
@DisplayName("이메일 전송을 보낼 수 있다.(enum 객체를 통한 전송)")
void sendEmail3() {
    NotificationRequest2 notificationRequest2 =
            NotificationRequest2.builder().type(NotificationType.EMAIL).message("이메일 보냅니다")
                    .target("teest@gmail.com").build();
    String sendType = notificationService.send3(notificationRequest2);
    assertThat(sendType).isEqualTo("EMAIL");
}

4. Enum 다형성의 장점 요약

  • 각 타입별 책임(유효성 검증, 전송 로직)의 명확한 분리가 됩니다.
  • 새로운 타입 추가 시, OCP(Open-Closed Principle) 를 따르는 구조가 가능해집니다.
  • Service 단의 코드가 가볍고 깔끔해집니다.
  • 테스트 작성이 용이하고 예외 케이스 대응이 쉬워집니다.

 

Enum 다형성을 활용하면 알림 로직처럼 타입별 동작이 명확히 구분되는 시나리오에서 훨씬 더 유지보수성이 뛰어난 코드를 만들 수 있습니다. 향후 알림 타입이 늘어나더라도 더 이상 if-else에 갇히지 않고 유연하게 확장 가능한 구조를 유지할 수 있습니다.

 

실제로도 팀원들이 가독성이 좋다고 얘기하고 추가되는 타입에 금방 대응하는 걸 확인하면서 뿌듯했습니다.

 

필요하신 분들은 전체 코드는 여기서 확인해주세요

 

 

blog-code/notification_polymorphic at main · jjjwodls/blog-code

Contribute to jjjwodls/blog-code development by creating an account on GitHub.

github.com

 

 

이상입니다! 읽어주셔서 감사합니다!

 

'Java & Spring' 카테고리의 다른 글

@EventListener, @TransactionalEventListener 설에 따른 이벤트 동작 방식  (3) 2025.08.13
Awaitility로 비동기 이벤트 테스트 하기: Spring @Async와 함께 쓰는 법  (3) 2025.07.29
Spring Cloud Config 도입기: 구성부터 실시간 설정 반영까지  (3) 2025.07.24
Spring Rest Docs 적용 3 - Controller Test 작성을 통한 문서화  (2) 2025.07.22
Spring Rest Docs 적용 2 - Swagger 연동  (1) 2025.07.21
'Java & Spring' 카테고리의 다른 글
  • @EventListener, @TransactionalEventListener 설에 따른 이벤트 동작 방식
  • Awaitility로 비동기 이벤트 테스트 하기: Spring @Async와 함께 쓰는 법
  • Spring Cloud Config 도입기: 구성부터 실시간 설정 반영까지
  • Spring Rest Docs 적용 3 - Controller Test 작성을 통한 문서화
jaess
jaess
jaess 님의 블로그 입니다.
  • jaess
    개발하는 개발자
    jaess
  • 전체
    오늘
    어제
    • 분류 전체보기 (9)
      • 회고 (1)
      • API 설계 (1)
      • 생각정리 (0)
      • Java & Spring (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    백엔드개발
    ifelse지옥탈출
    Spring Cloud Config Server
    @Async
    OOP (Object Oriented Programming)
    CleanCode
    Spring Cloud Config Client
    Spring Actuator
    분기처리개선
    RestDoc
    비동기 메시징
    Spring boot
    junit5
    Spring Event
    비동기 테스트
    Spring cloud
    Git 설정 관리
    Spring
    무중단설정변경
    REST API
    springboot
    java
    Awaitility
    스프링이벤트
    코드리팩토링
    개발자
    TransactionalEventListener
    이벤트 리스너
    비동기이벤트
    Spring Cloud Config
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
jaess
Java Enum 다형성으로 Notification 처리 리팩토링하기 - 조건문 없는 전략 설계
상단으로

티스토리툴바