ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,54 @@
+package store.domain.vo;
+
+import store.infrastructure.constant.ExceptionMessage;
+import store.infrastructure.exception.CustomException;
+
+public record Price(int value) implements Comparable<Price> {
+    public static final int MIN = 10;
+    public static final int MAX = 1_000_000;
+
+    public Price {
+        validatePrice(value);
+    }
+
+    public static Price from(String input) {
+        try {
+            return new Price(Integer.parseInt(input.trim()));
+        } catch (NumberFormatException e) {
+            throw new CustomException(ExceptionMessage.WRONG_INTEGER_FORMAT.message());
+        }
+    }
+
+    private void validatePrice(int value) {
+        if (value < MIN || value > MAX) {
+            throw new CustomException(ExceptionMessage.WRONG_PRICE_RANGE.message());
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof Integer) {
+            return (Integer) o == value;
+        }
+        if (o instanceof Price) {
+            return ((Price) o).value == value;
+        }
+        return false;
+    }
+
+    @Override
+    public int compareTo(Price other) {
+        return value - other.value;
+    }
+
+    public int multiply(Quantity quantity) {
+        return quantity.value() * value;
+    }
+
+    public int multiply(int other) {
+        if (other <= 0) {
+            throw new IllegalStateException("0 이하의 숫자는 가격에 곱할 수 없습니다.");
+        }
+        return other * value;
+    }
+}

리뷰: 일단 `record`의 경우 `private` 생성자를 선언하더라도, 아래와 같이 직접 생성하는 것을 막을 수 없다고 알고있습니다!
```java
Price price = new Price(1000);
```
무엇보다 제가 의도했던 것은 **정수 값 뿐만 아니라, 정수를 담는 String 객체로도 `Price 객체`를 생성할 수 있도록 하는 것**이었습니다.
- 즉, **int 자료형**으로만 `Price 객체`를 생성하는 것이 아닌, **String 자료형**으로도 `Price 객체`를 생성할 수 있도록 의도했습니다!
@@ -0,0 +1,6 @@
+package store.infrastructure.constant;
+
+public class Membership {
+    public static final int DISCOUNT_RATE = 30;
+    public static final int MAX = 8_000;
+}

리뷰: 좋은 질문인 것 같습니다!

### 1. 상수화 여부 결정
> 저는 우선 몇 가지 분류 기준으로 **상수화를 결정**합니다.
- 첫 번째 분류 기준은 `서비스 요구사항과의 밀접도`인 것 같습니다.
**서비스 요구사항**은 변동 가능성이 큰 부분이라고 생각해서, **_이와 밀접하게 연관된 값들은 따로 상수로 관리하는 것_** 이 좋다고 생각해요.
👉🏻 예를들어 `멤버십 할인율`이나 `최대 할인 가능 금액`은 서비스 요구사항과 밀접한 연관이 있으므로 상수로 관리합니다.

- 두 번째는 `의미가 표현되지 않는 값`입니다.
**의미를 포함하지 않는 값**을 그대로 사용하면, 코드를 이해하는데 있어서도 어려움이 있다고 생각해요.
👉🏻 예를들어 콤마(`,`), 대시(`-`) 등과 같은 문자는 문자 자체로 의미가 표현되지 않습니다. 동시에 이 값들은 `입출력 요구사항`과도 밀접한 연관이 있기 때문에, 상수화를 결정했습니다.

### 2. 상수 위치
> 그리고 이렇게 상수화한 값들을 `별도의 클래스에서 관리`할 것인지, `관련 클래스 내부에서 관리`할 것인지도 많이 고민하게 되는데요,
- 우선 값 그 자체의 조건에 대한 상수 값 _(ex. price의 최대, 최소 값)_ 들은 보통 `vo 객체` 내부에서 관리합니다. 
👉🏻 값에 대한 `검증 조건`과 `검증 과정`을 모두 vo 객체 내부에서 관리하는 것이 **응집도** 측면에서 좋다고 생각하기 때문이에요.

- 그 외에 서비스 요구사항 관련 상수 값들은 도메인 외부에 클래스로 묶어서 관리합니다.
👉🏻 예를들어 `멤버십 할인율` 및 `최대 할인 금액`의 경우, 값 자체보다는 `서비스 로직을 처리하는 과정`에 제한을 두는 역할에 가깝다고 생각했습니다.

특히, 멤버십과 같이 **서비스 로직을 처리하는 과정에 필요한 상수 값들**을 도메인 객체 내부에서 관리하기보다는, 서비스 계층과 동일한 레벨에서(= `infrastructure`) 관리하는 것이 더 직관적이라고 생각했어요.

### 정리
설명이 너무 장황해진 것 같은데.. 정리하자면 
- 서비스 로직 처리 과정에 필요한 값들은 보통 외부에 클래스로 분리해서 관리하는 편이고, 
- 도메인 객체 값과 밀접한 값들은 도메인 객체 내부에서 관리하는 것 같습니다!
@@ -0,0 +1,25 @@
+package nextstep.security.authentication;
+
+import nextstep.app.util.Base64Convertor;
+import nextstep.security.authentication.exception.AuthenticationException;
+import nextstep.security.user.UsernamePasswordAuthenticationToken;
+import org.springframework.util.StringUtils;
+
+public class AuthenticationConverter {
+
+
+    public Authentication convert(String headerValue) {
+        if (!StringUtils.hasText(headerValue)) {
+            throw new AuthenticationException();
+        }
+
+        String credentials = headerValue.split(" ")[1];
+        String decodedString = Base64Convertor.decode(credentials);
+        String[] usernameAndPassword = decodedString.split(":");
+        if (usernameAndPassword.length != 2) {
+            throw new AuthenticationException();
+        }
+
+        return UsernamePasswordAuthenticationToken.unAuthorizedToken(usernameAndPassword[0], usernameAndPassword[1]);
+    }
+}

리뷰: Convert의 책임을 분리해주셨군요 👍
@@ -0,0 +1,28 @@
+package nextstep.security.authentication;
+
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.authentication.exception.AuthenticationException;
+import nextstep.security.authentication.exception.MemberAccessDeniedException;
+
+import java.io.IOException;
+
+public class AuthenticationErrorHandler {
+
+    protected AuthenticationErrorHandler() {
+        throw new UnsupportedOperationException();
+    }
+
+    public static void handleError(HttpServletResponse response, RuntimeException e) throws IOException {
+        if (e instanceof MemberAccessDeniedException) {
+            response.sendError(HttpServletResponse.SC_FORBIDDEN);
+            return;
+        }
+
+        if (e instanceof AuthenticationException) {
+            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+            return;
+        }
+
+        response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+}

리뷰: error handle의 책임도 분리해주셨군요 👍
@@ -0,0 +1,75 @@
+package nextstep.security.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.AuthenticationConverter;
+import nextstep.security.authentication.AuthenticationErrorHandler;
+import nextstep.security.authentication.AuthenticationManager;
+import nextstep.security.authentication.exception.AuthenticationException;
+import nextstep.security.context.SecurityContextHolder;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+
+public class BasicAuthenticationFilter extends OncePerRequestFilter {
+
+    private final AuthenticationManager authenticationManager;
+    private final AuthenticationConverter converter = new AuthenticationConverter();
+
+    private static final String AUTHORIZATION_HEADER = "Authorization";
+    private static final String[] BASIC_AUTH_PATH = new String[]{"/members"};
+
+    public BasicAuthenticationFilter(AuthenticationManager authenticationManager) {
+        this.authenticationManager = Objects.requireNonNull(authenticationManager);
+    }
+
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        boolean isNotGetMethod = !HttpMethod.GET.name().equalsIgnoreCase(request.getMethod());
+        boolean shouldNotFilterURI = Arrays.stream(BASIC_AUTH_PATH).noneMatch(it -> it.equalsIgnoreCase(request.getRequestURI()));
+        return isNotGetMethod || shouldNotFilterURI;
+    }
+
+    @Override
+    protected void doFilterInternal(
+            HttpServletRequest request,
+            HttpServletResponse response,
+            FilterChain filterChain
+    ) throws IOException, ServletException {
+
+        try {
+            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+            if (authentication != null) {
+                filterChain.doFilter(request, response);
+                return;
+            }
+
+            authentication = authenticateByRequestHeader(request);
+
+            SecurityContextHolder.getContext().setAuthentication(authentication);
+
+        } catch (AuthenticationException e) {
+            AuthenticationErrorHandler.handleError(response, e);
+            return;
+        }
+
+        filterChain.doFilter(request, response);
+    }
+
+    private Authentication authenticateByRequestHeader(HttpServletRequest request) {
+        Authentication authentication = converter.convert(request.getHeader(AUTHORIZATION_HEADER));
+
+        Authentication result = authenticationManager.authenticate(authentication);
+        if (!result.isAuthenticated()) {
+            throw new AuthenticationException();
+        }
+
+        return result;
+    }
+}

리뷰: OncePerRequestFilter 상속을 잘 해주셨습니다!
OncePerRequestFilter를 활용하신 이유는 무엇인가요?
@@ -4,7 +4,14 @@
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
 import roomit.main.domain.business.dto.CustomBusinessDetails;
 import roomit.main.domain.business.dto.request.BusinessRegisterRequest;
 import roomit.main.domain.business.dto.request.BusinessUpdateRequest;
@@ -13,38 +20,39 @@
 
 
 @RestController
+@RequestMapping("/api/v1/business")
 @RequiredArgsConstructor
 public class BusinessController {
 
     private final BusinessService businessService;
 
     //사업자 회원 가입
     @ResponseStatus(HttpStatus.CREATED)
-    @PostMapping("/api/v1/business/signup")
-    public void signup(@RequestBody @Valid BusinessRegisterRequest request) {
+    @PostMapping("/signup")
+    public void signUp(@RequestBody @Valid BusinessRegisterRequest request) {
         businessService.signUpBusiness(request);
     }
 
     //사업자 정보 수정
     @ResponseStatus(HttpStatus.NO_CONTENT)
-    @PutMapping("api/v1/business")
-    public void businessModify(@RequestBody @Valid BusinessUpdateRequest businessUpdateRequest
-                               ,@AuthenticationPrincipal CustomBusinessDetails customBusinessDetails){
+    @PutMapping
+    public void businessModify(@RequestBody @Valid BusinessUpdateRequest businessUpdateRequest,
+                               @AuthenticationPrincipal CustomBusinessDetails customBusinessDetails){
         businessService.updateBusinessInfo(customBusinessDetails.getId(), businessUpdateRequest);
     }
 
 
     //사업자 정보 조회
     @ResponseStatus(HttpStatus.OK)
-    @GetMapping("api/v1/business")
+    @GetMapping
     public BusinessResponse businessRead(@AuthenticationPrincipal CustomBusinessDetails customBusinessDetails){
         return businessService.readBusinessInfo(customBusinessDetails.getId());
     }
 
 
     //사업자 탈퇴
     @ResponseStatus(HttpStatus.NO_CONTENT)
-    @DeleteMapping("api/v1/business")
+    @DeleteMapping
     public void businessDelete(@AuthenticationPrincipal CustomBusinessDetails customBusinessDetails){
         businessService.deleteBusiness(customBusinessDetails.getId());
     }

리뷰: 사소한거긴 한데, 사실 이런 경우엔 그냥 `@PutMapping` 을 써도 됩니다.
@@ -1,15 +1,40 @@
 package roomit.main.domain.business.repository;
 
+import java.util.Optional;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
 import roomit.main.domain.business.entity.Business;
 
-import java.util.Optional;
-
 public interface BusinessRepository extends JpaRepository<Business, Long> {
 
-    @Query(value = "SELECT b FROM Business b WHERE b.businessEmail.value=:email")
-    Optional<Business> findByBusinessEmail(String email);
-  
+    @Query(value = "SELECT b FROM Business b WHERE b.businessEmail.value=:businessEmail")
+    Optional<Business> findByBusinessEmail(String businessEmail);
+
+    @Query("""
+	    SELECT CASE WHEN EXISTS (
+	        SELECT 1
+	        FROM Business b
+	        WHERE b.businessName.value = :businessName
+	    ) THEN TRUE ELSE FALSE END
+	""")
+    Boolean existsByBusinessName(String businessName);
+
+    @Query("""
+	    SELECT CASE WHEN EXISTS (
+	        SELECT 1
+	        FROM Business b
+	        WHERE b.businessEmail.value = :businessEmail
+	    ) THEN TRUE ELSE FALSE END
+	""")
+    Boolean existsByBusinessEmail(String businessEmail);
+
+    @Query("""
+	    SELECT CASE WHEN EXISTS (
+	        SELECT 1
+	        FROM Business b
+	        WHERE b.businessNum.value = :businessNum
+	    ) THEN TRUE ELSE FALSE END
+	""")
+    Boolean existsByBusinessNum(String businessNum);
 }
 

리뷰: 존재 여부를 확인하는 건가요?
이 경우, `EXIST` 을 걸면 좋습니다.
한 개 검색이 되는 순간 쿼리를 종료시켜 버리거든요.

```java
@Query("""
	    SELECT CASE WHEN EXISTS (
	        SELECT 1 
	        FROM Business b
	        WHERE b.businessId = :senderId AND b.businessName = :senderName
	    ) THEN TRUE ELSE FALSE END
	""")
boolean existsByIdAndBusinessName(Long senderId, String senderName);
```
@@ -1,15 +1,40 @@
 package roomit.main.domain.business.repository;
 
+import java.util.Optional;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
 import roomit.main.domain.business.entity.Business;
 
-import java.util.Optional;
-
 public interface BusinessRepository extends JpaRepository<Business, Long> {
 
-    @Query(value = "SELECT b FROM Business b WHERE b.businessEmail.value=:email")
-    Optional<Business> findByBusinessEmail(String email);
-  
+    @Query(value = "SELECT b FROM Business b WHERE b.businessEmail.value=:businessEmail")
+    Optional<Business> findByBusinessEmail(String businessEmail);
+
+    @Query("""
+	    SELECT CASE WHEN EXISTS (
+	        SELECT 1
+	        FROM Business b
+	        WHERE b.businessName.value = :businessName
+	    ) THEN TRUE ELSE FALSE END
+	""")
+    Boolean existsByBusinessName(String businessName);
+
+    @Query("""
+	    SELECT CASE WHEN EXISTS (
+	        SELECT 1
+	        FROM Business b
+	        WHERE b.businessEmail.value = :businessEmail
+	    ) THEN TRUE ELSE FALSE END
+	""")
+    Boolean existsByBusinessEmail(String businessEmail);
+
+    @Query("""
+	    SELECT CASE WHEN EXISTS (
+	        SELECT 1
+	        FROM Business b
+	        WHERE b.businessNum.value = :businessNum
+	    ) THEN TRUE ELSE FALSE END
+	""")
+    Boolean existsByBusinessNum(String businessNum);
 }
 

리뷰: 이것만 primitive 네요.
@@ -0,0 +1,23 @@
+package roomit.main.domain.chat.chatmessage.service;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import roomit.main.domain.chat.chatroom.repository.ChatRoomRepository;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class ChatMessageBatchProcessor {
+    private final ChatService chatService;
+    private final ChatRoomRepository chatRoomRepository;
+
+    @Scheduled(fixedRate = 30000) // 1분마다 실행
+    public void flushMessages() {
+        List<Long> roomIds = chatRoomRepository.findAllRoomIds(); // Room ID 동적 조회
+        for (Long roomId : roomIds) {
+            chatService.flushMessagesToDatabase(roomId);
+        }
+    }
+}

리뷰: roomId 의 수가 끝도 없이 많을 수 있지 않을까요?
@@ -0,0 +1,56 @@
+package store.config;
+
+import java.util.Map;
+import store.controller.ConvenienceController;
+import store.convenience.Convenience;
+import store.domain.Product;
+import store.domain.Promotion;
+import store.reader.StoreFileReader;
+import store.repository.ProductRepository;
+import store.repository.PromotionRepository;
+import store.service.ConvenienceService;
+import store.service.ProductService;
+import store.service.PromotionService;
+import store.view.InputView;
+import store.view.OutputView;
+
+public class ConvenienceFactory {
+
+    public static Convenience createConvenience() {
+        return new Convenience(convenienceController(), outputView(), inputView());
+    }
+
+    public static ConvenienceController convenienceController() {
+        return new ConvenienceController(convenienceService());
+    }
+
+    public static ConvenienceService convenienceService() {
+        return new ConvenienceService(productService(), promotionService());
+    }
+
+    public static OutputView outputView() {
+        return new OutputView();
+    }
+
+    public static InputView inputView() {
+        return new InputView();
+    }
+
+    public static ProductRepository productRepository() {
+        Map<String, Promotion> promotions = StoreFileReader.promotions();
+        Map<String, Product> products = StoreFileReader.product(promotions);
+        return new ProductRepository(products);
+    }
+
+    public static ProductService productService() {
+        return new ProductService(productRepository());
+    }
+
+    public static PromotionService promotionService() {
+        return new PromotionService(promotionRepository());
+    }
+
+    public static PromotionRepository promotionRepository() {
+        return new PromotionRepository(StoreFileReader.promotions());
+    }
+}

리뷰: 저도 2주차에 Factory를 사용했었는데 확실히 의존성 주입하기에는 이것만큼 좋은게 없는 것 같아요
@@ -0,0 +1,56 @@
+package store.config;
+
+import java.util.Map;
+import store.controller.ConvenienceController;
+import store.convenience.Convenience;
+import store.domain.Product;
+import store.domain.Promotion;
+import store.reader.StoreFileReader;
+import store.repository.ProductRepository;
+import store.repository.PromotionRepository;
+import store.service.ConvenienceService;
+import store.service.ProductService;
+import store.service.PromotionService;
+import store.view.InputView;
+import store.view.OutputView;
+
+public class ConvenienceFactory {
+
+    public static Convenience createConvenience() {
+        return new Convenience(convenienceController(), outputView(), inputView());
+    }
+
+    public static ConvenienceController convenienceController() {
+        return new ConvenienceController(convenienceService());
+    }
+
+    public static ConvenienceService convenienceService() {
+        return new ConvenienceService(productService(), promotionService());
+    }
+
+    public static OutputView outputView() {
+        return new OutputView();
+    }
+
+    public static InputView inputView() {
+        return new InputView();
+    }
+
+    public static ProductRepository productRepository() {
+        Map<String, Promotion> promotions = StoreFileReader.promotions();
+        Map<String, Product> products = StoreFileReader.product(promotions);
+        return new ProductRepository(products);
+    }
+
+    public static ProductService productService() {
+        return new ProductService(productRepository());
+    }
+
+    public static PromotionService promotionService() {
+        return new PromotionService(promotionRepository());
+    }
+
+    public static PromotionRepository promotionRepository() {
+        return new PromotionRepository(StoreFileReader.promotions());
+    }
+}

리뷰: IO작업의 경우 여러 개의 인스턴스가 한번에 접근한다면 문제가 생길수 있다고 생각합니다. getInstance메서드를 통해 싱글톤 유지해보는것은 어떨까요?
@@ -0,0 +1,56 @@
+package store.config;
+
+import java.util.Map;
+import store.controller.ConvenienceController;
+import store.convenience.Convenience;
+import store.domain.Product;
+import store.domain.Promotion;
+import store.reader.StoreFileReader;
+import store.repository.ProductRepository;
+import store.repository.PromotionRepository;
+import store.service.ConvenienceService;
+import store.service.ProductService;
+import store.service.PromotionService;
+import store.view.InputView;
+import store.view.OutputView;
+
+public class ConvenienceFactory {
+
+    public static Convenience createConvenience() {
+        return new Convenience(convenienceController(), outputView(), inputView());
+    }
+
+    public static ConvenienceController convenienceController() {
+        return new ConvenienceController(convenienceService());
+    }
+
+    public static ConvenienceService convenienceService() {
+        return new ConvenienceService(productService(), promotionService());
+    }
+
+    public static OutputView outputView() {
+        return new OutputView();
+    }
+
+    public static InputView inputView() {
+        return new InputView();
+    }
+
+    public static ProductRepository productRepository() {
+        Map<String, Promotion> promotions = StoreFileReader.promotions();
+        Map<String, Product> products = StoreFileReader.product(promotions);
+        return new ProductRepository(products);
+    }
+
+    public static ProductService productService() {
+        return new ProductService(productRepository());
+    }
+
+    public static PromotionService promotionService() {
+        return new PromotionService(promotionRepository());
+    }
+
+    public static PromotionRepository promotionRepository() {
+        return new PromotionRepository(StoreFileReader.promotions());
+    }
+}

리뷰: Factory의 장점은 의존성 주입이라고 생각합니다. 인터페이스를 통해 클래스간 결합도를 줄여보는 것은 어떨까요?
@@ -0,0 +1,56 @@
+package store.config;
+
+import java.util.Map;
+import store.controller.ConvenienceController;
+import store.convenience.Convenience;
+import store.domain.Product;
+import store.domain.Promotion;
+import store.reader.StoreFileReader;
+import store.repository.ProductRepository;
+import store.repository.PromotionRepository;
+import store.service.ConvenienceService;
+import store.service.ProductService;
+import store.service.PromotionService;
+import store.view.InputView;
+import store.view.OutputView;
+
+public class ConvenienceFactory {
+
+    public static Convenience createConvenience() {
+        return new Convenience(convenienceController(), outputView(), inputView());
+    }
+
+    public static ConvenienceController convenienceController() {
+        return new ConvenienceController(convenienceService());
+    }
+
+    public static ConvenienceService convenienceService() {
+        return new ConvenienceService(productService(), promotionService());
+    }
+
+    public static OutputView outputView() {
+        return new OutputView();
+    }
+
+    public static InputView inputView() {
+        return new InputView();
+    }
+
+    public static ProductRepository productRepository() {
+        Map<String, Promotion> promotions = StoreFileReader.promotions();
+        Map<String, Product> products = StoreFileReader.product(promotions);
+        return new ProductRepository(products);
+    }
+
+    public static ProductService productService() {
+        return new ProductService(productRepository());
+    }
+
+    public static PromotionService promotionService() {
+        return new PromotionService(promotionRepository());
+    }
+
+    public static PromotionRepository promotionRepository() {
+        return new PromotionRepository(StoreFileReader.promotions());
+    }
+}

리뷰: staitc 메서드를 통해 인스턴스를 생성하지 않는 형태로 만드셨는데 private생성자를 통해 기본 생성자를 생성하는 것을 막는다면 더 좋을 것 같습니다.
@@ -0,0 +1,19 @@
+package store.config;
+
+public enum ErrorMessage {
+
+    PRODUCT_NOT_EXIST("존재하지 않는 상품입니다. 다시 입력해 주세요."),
+    PRODUCT_QUANTITY_NOT_ENOUGH("재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요."),
+    PRODUCT_WRONG_INPUT("올바르지 않은 형식으로 입력했습니다. 다시 입력해 주세요."),
+    WRONG_INPUT("잘못된 입력입니다. 다시 입력해 주세요.");
+
+    private final String message;
+
+    ErrorMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return "[ERROR] " + message;
+    }
+}

리뷰: "[ERROR] "라는 것을 필드에 넣어 상수로 만든다면 더 좋을 것 같습니다.
```
private static final String ERROR_PREFIX = "[ERROR] ";
```