Spring에서 컨트롤러를 지정해주기 위한 어노테이션은 @Controller와 @RestController가 있습니다. 전통적인 Spring MVC의 컨트롤러인 @Controller와 Restuful 웹서비스의 컨트롤러인 @RestController의 주요한 차이점은 HTTP Response Body가 생성되는 방식입니다. 이번에는 2가지 어노테이션의 차이와 사용법에 대해 알아보도록 하겠습니다.

 

 

 

1. @Controller 이해하기


[ Controller로 View 반환하기 ]

전통적인 Spring MVC의 컨트롤러인 @Controller는 주로 View를 반환하기 위해 사용합니다. 아래와 같은 과정을 통해 Spring MVC Container는 Client의 요청으로부터 View를 반환합니다.

 

 

 

 

  1. Client는 URI 형식으로 웹 서비스에 요청을 보낸다.
  2. DispatcherServlet이 요청을 위임할 HandlerMapping을 찾는다.
  3. HandlerMapping을 통해 요청을 Controller로 위임한다.
  4. Controller는 요청을 처리한 후에 ViewName을 반환한다.
  5. DispatcherServlet은 ViewResolver를 통해 ViewName에 해당하는 View를 찾아 사용자에게 반환한다.


Controller가 반환환 뷰의 이름으로부터 View를 렌더링하기 위해서는 ViewResolver가 사용되며, ViewResolver 설정에 맞게 View를 찾아 렌더링합니다.

 

 

 

 

[ Controller로 Data 반환하기 ]

하지만 Spring MVC의 컨트롤러를 사용하면서 Data를 반환해야 하는 경우도 있습니다. 컨트롤러에서는 데이터를 반환하기 위해 @ResponseBody 어노테이션을 활용해주어야 합니다. 이를 통해 Controller도 Json 형태로 데이터를 반환할 수 있습니다.

 

 

 

 

  1. Client는 URI 형식으로 웹 서비스에 요청을 보낸다.
  2. DispatcherServlet이 요청을 위임할 HandlerMapping을 찾는다.
  3. HandlerMapping을 통해 요청을 Controller로 위임한다.
  4. Controller는 요청을 처리한 후에 객체를 반환한다.
  5. 반환되는 객체는 Json으로 Serialize되어 사용자에게 반환된다.

 

 

컨트롤러를 통해 객체를 반환할 때에는 일반적으로 ResponseEntity로 감싸서 반환을 합니다. 그리고 객체를 반환하기 위해서는 viewResolver 대신에 HttpMessageConverter가 동작합니다. HttpMessageConverter에는 여러 Converter가 등록되어 있고, 반환해야 하는 데이터에 따라 사용되는 Converter가 달라집니다. 단순 문자열인 경우에는 StringHttpMessageConverter가 사용되고, 객체인 경우에는 MappingJackson2HttpMessageConverter가 사용되며, 데이터 종류에 따라 서로 다른 MessageConverter가 작동하게 됩니다. Spring은 클라이언트의 HTTP Accept 헤더와 서버의 컨트롤러 반환 타입 정보 둘을 조합해 적합한 HttpMessageConverter를 선택하여 이를 처리합니다.

 

 

 

[ @Controller 예제 코드 ]

@Controller
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping(value = "/users")
    public @ResponseBody ResponseEntity<User> findUser(@RequestParam("userName") String userName){
        return ResponseEntity.ok(userService.findUser(user));
    }
    
    @GetMapping(value = "/users/detailView")
    public String detailView(Model model, @RequestParam("userName") String userName){
        User user = userService.findUser(userName);
        model.addAttribute("user", user);
        return "/users/detailView";
    }
}

 

 

위 예제의 findUser는 User 객체를 ResponseEntity로 감싸서 반환하고 있고, User를 json으로 반환하기 위해 @ResponseBody라는 어노테이션을 붙여주고 있습니다. detailView 함수에서는 View를 전달해주고 있기 때문에 String을 반환값으로 설정해주었습니다. (물론 이렇게 데이터를 반환하는 RestController와 View를 반환하는 Controller를 분리하여 작성하는 것이 좋습니다.)

 

 

 

 

2. @RestController 이해하기


[ RestController ]

@RestController는 @Controller에 @ResponseBody가 추가된 것입니다. 당연하게도 RestController의 주용도는 Json 형태로 객체 데이터를 반환하는 것입니다. 최근에 데이터를 응답으로 제공하는 REST API를 개발할 때 주로 사용하며 객체를 ResponseEntity로 감싸서 반환합니다. 이러한 이유로 동작 과정 역시 @Controller에 @ReponseBody를 붙인 것과 완벽히 동일합니다.

 

 

  1. Client는 URI 형식으로 웹 서비스에 요청을 보낸다.
  2. DispatcherServlet이 요청을 위임할 HandlerMapping을 찾는다.
  3. HandlerMapping을 통해 요청을 Controller로 위임한다.
  4. Controller는 요청을 처리한 후에 객체를 반환한다.
  5. 반환되는 객체는 Json으로 Serialize되어 사용자에게 반환된다.

 

 

 

[ @RestController 예제 코드 ]

@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping(value = "/users")
    public User findUser(@RequestParam("userName") String userName){
        return userService.findUser(user);
    }

    @GetMapping(value = "/users")
    public ResponseEntity<User> findUserWithResponseEntity(@RequestParam("userName") String userName){
        return ResponseEntity.ok(userService.findUser(user));
    }
}

 

 

findUser는 User 객체를 그대로 반환하고 있습니다. 이러한 경우의 문제는 클라이언트가 예상하는 HttpStatus를 설정해줄 수 없다는 것입니다. 예를 들어 어떤 객체의 생성 요청이라면 201 CREATED를 기대할 것이지만 객체를 그대로 반환하면 HttpStatus를 설정해줄 수 없습니다. 그래서 객체를 상황에 맞는 ResponseEntity로 감싸서 반환해주어야 합니다.

출처: https://mangkyu.tistory.com/49 [MangKyu's Diary:티스토리]

Lombok 사용상 주의점(Pitfall)

Lombok은 그 강력한 기능만큼 사용상 주의를 요한다.

현재 1.16 버전 기준.

@AllArgsConstructor, @RequiredArgsConstructor 사용금지

@AllArgsConstructor, @RequiredArgsConstructor 는 매우 편리하게 생성자를 만들어주지만 개발자의 별 생각없는 꼼꼼함이 치명적 버그가 되게 만들 수 있다.

@AllArgsConstructor
public static class Order {
    private long cancelPrice;
    private long orderPrice;
}
 
// 취소금액 5,000원, 주문금액 10,000원
Order order = new Order(5000L, 10000L); 

위 클래스에 대해 자동으로 cancelPrice, orderPrice 순서로 인자를 받는 생성자가 만들어진다. 그런데 개발자가 보기에 Order 클래스인데 cancelPrice가 orderPrice보다 위에 있는것이 마음에 안들어서 순서를 다음과 같이 바꾼다고 해보자.

@AllArgsConstructor
public static class Order {
    private long orderPrice;
    private long cancelPrice;
}

이 경우, IDE가 제공해주는 리팩토링은 전혀 작동하지 않고, lombok이 개발자도 인식하지 못하는 사이에 생성자의 파라미터 순서를 필드 선언 순서에 맞춰 orderPrice,cancelPrice로 바꿔버린다. 게다가 이 두 필드는 동일한 Type 이라서 기존 생성자호출 코드에서는 인자 순서를 변경하지 않았음에도 어떠한 오류도 발생하지 않는다.

이에 의해, 위의 생성자를 호출하는 코드는 아무런 에러없이 잘 작동하는 듯 보이지만 실제로 입력된 값은 바뀌어 들어가게 된다.

// 주문금액 5,000원, 취소금액 10,000원. 취소금액이 주문금액보다 많아짐!
Order order = new Order(5000L, 10000L); // 인자값의 순서 변경 없음

이 문제는 @AllArgsConstructor와 @RequiredArgsConstructor에 둘 다 존재하며, 이에 따라 이 두 lombok 애노테이션은 사용을 금지하는 것이 좋다.

대신, 생성자를 (IDE 자동생성등으로) 직접 만들고 필요할 경우에는 직접 만든 생성자에 @Builder 애노테이션을 붙이는 것을 권장한다. 파라미터 순서가 아닌 이름으로 값을 설정하기 때문에 리팩토링에 유연하게 대응할 수 있다.

public static class Order {
    private long cancelPrice;
    private long orderPrice;
 
    @Builder
    private Order(long cancelPrice, long orderPrice) {
        this.cancelPrice = cancelPrice;
        this.orderPrice = orderPrice;
    }
}
 
// 필드 순서를 변경해도 문제 없음.
Order order = Order.builder().cancelPrice(5000L).orderPrice(10000L).build();
System.out.println(order);

필드에 붙은 애노테이션이 생성자 쪽으로 전달 안됨

  • 필드에 애노테이션이 있을 경우 자동으로 생성자로 전달이 안 되어 문제가 될 수 있다.
  • 특히 @Data,@AllArgs… 등을 사용하면 이를 잊어버리는 경우가 부지기수 이다.
  • @XXXArgsConstructor(onConstructor=@__(@원하는애노테이션)) 를 사용할 경우 생성자의 각 인자에 애노테이션이 붙는게 아니라 생성자 자체에 애노테이션이 붙는다.

무분별한 @EqualsAndHashCode 사용 자제

@EqualsAndHashCode는 상당히 고품질의 equals, hashCode 메소드를 만들어준다. 잘 사용하면 좋지만 남발하면 심각한 문제가 생긴다.

특히 문제가 되는 점은 바로 Mutable(변경가능한) 객체에 아무런 파라미터 없이 그냥 사용하는 @EqualsAndHashCode 애노테이션이다.

@EqualsAndHashCode
public static class Order {
    private Long orderId;
    private long orderPrice;
    private long cancelPrice;
 
    public Order(Long orderId, long orderPrice, long cancelPrice) {
        this.orderId = orderId;
        this.orderPrice = orderPrice;
        this.cancelPrice = cancelPrice;
    }
}
 
Order order = new Order(1000L, 19800L, 0L);
 
Set<Order> orders = new HashSet<>();
orders.add(order); // Set에 객체 추가
 
System.out.println("변경전 : " + orders.contains(order)); // true
 
order.setCancelPrice(5000L); // cancelPrice 값 변경
System.out.println("변경후 : " + orders.contains(order)); // false

위와 같이 동일한 객체임이도 Set 에 저장한 뒤에 필드 값을 변경하면 hashCode가 변경되면서 찾을 수 없게 되어버린다. 이는 @EqualsAndHashCode의 문제라기 보다는 변경가능한 필드에 이를 남발함으로써 생기는 문제이다.

따라서,

  • Immutable(불변) 클래스를 제외하고는 아무 파라미터 없는 @EqualsAndHashCode 사용은 금지한다.
  • 일반적으로 비교에서 사용하지 않는 Data 성 객체는 equals & hashCode를 따로 구현하지 않는게 차라리 낫다.
  • 항상 @EqualsAndHashCode(of={“필드명시”}) 형태로 동등성 비교에 필요한 필드를 명시하는 형태로 사용한다.
  • 실전에서는 누군가는 이에 대해 실수하기 마련인지라 차라리 사용을 완전히 금지시키고 IDE 자동생성으로 꼭 필요한 필드를 지정하는 것이 나을 수도 있다.
  • Java equals & hashCode 좋은 equals 만드는 방법
  • 막상 개발을 하다보면 온전히 Immutable 필드를 대상으로만 equals & hashCode를 만들기는 매우 어렵다. 최소한 꼭 필요하고 일반적으로 변하지 않는 필드에 대해서만 만들도록 노력해야 한다.
  • Equals Verifier를 통해 equals, hashCode 메소드 테스트를 자동으로 할 수 있다.

나는 위와 같은 이유로 Apache Commons EqualsBuilder reflectionEquals도 사용하지 말 것을 권한다.

@Data 사용금지

@Data는 파라미터 없는 @EqualsAndHashCode와 @RequiredArgsConstructor 등을 포함하는 Mutable한 데이터 클래스를 만들어주는 조합형 애노테이션이다.

바로 @EqualsAndHashCode와 @RequiredArgsConstructor를 포함하기 때문에 사용을 아예 금지하고, 차라리 다음과 같이 명시하는 것이 좋다.

@Getter
@Setter
@ToString
public class Order {
...
    // 생성자와 필요한 경우에만 equals, hashCode 직접 작성
}

개발자에게 @Data 사용시 equals, hashCode 그리고 생성자를 직접 만들어주라고 “말해봐야” 아무 소용없다. 그냥 깔끔하게 금지시키는게 낫다.

@Value 사용금지

@Value는 Immutable 클래스를 만들어주는 조합 애노테이션이지만 이 또한 @EqualsAndHashCode, @AllArgsConstructor 를 포함한다. @EqualsAndHashCode는 불변 클래스라 큰 문제가 안되지만 @AllArgsConstructor가 문제가 된다.

아예 사용을 금지시키고 다음과 같이 만드는게 낫다.

@Getter
@ToString
public class Order {
   // private final 로 여러 필드 생성
    // 생성자와 필요한 경우에만 equals, hashCode 직접 작성
}

@Builder 를 생성자나 static 객체 생성 메소드에

@Builder 를 사용하면 객체 생성이 좀 더 명확하고 쉬워지는데, 이는 기본적으로 @AllArgsConstructor를 내포하고 있다. 이 자체는 평상시에는 큰 문제가 안된다. 생성자를 package private으로 만들기 때문에 외부에서 생성자를 호출하는 일은 쉽게 안생긴다. 하지만 그래도 해당 클래스의 다른 메소드에서 이렇게 자동으로 생성된 생성자를 사용하거나 할 때 문제 소지가 있다.

따라서 @Builder 애노테이션은 가급적 클래스 보다는 직접 만든 생성자 혹은 static 객체 생성 메소드에 붙이는 것을 권장한다. (생성자 부분에 예시 나옴)

@Log

@Log 를 통해 각종 Logger 를 자동생성 할 수 있다. 이 때 기본적으로 private static final로 생성하는데, static이 아닌 필드로 만들고자 하거나 Logger 객체 이름을 변경하고자 한다면 lombok.config를 사용하면 된다.

lombok.log.fieldName=logger # 로거 객체 이름을 logger로 변경. 원래는 log
lombok.log.fieldIsStatic=false # 로거를 static이 아닌 필드로 생성

또한, 가급적이면 @Slf4j만 사용하고 나머지는 사용할 수 없게 금지시키는 것도 가능하다. 자세한 것은 아래 lombok.config 항목에서 설명한다.

나는 (라이브러리성이 아닌) 일반 서비스에서는 Logger를 field 변수로 만들 필요 없이 static final로 하는 것을 선호한다. 그래야 별다른 처리 없이 static method에서도 호출 가능하기 때문이다.

@NonNull 사용 금지

@NonNull 사용금지는 사실 개인적 취향에 가까운데, 불필요하게 branch coverage를 증가시키기 때문이다(즉, 프로젝트 코드 커버리지를 유지하고 싶다면 null인 상황에서 오류발생과 그렇지 않은 상황에 대한 테스트를 모든 사용처에서 만들어야 한다). 그렇다고 이 코드를 일일이 테스트를 만들자니 이미 검증된 라이브러리의 기능을 사용하는 곳곳에서 모두 테스트를 만드는 것도 굉장히 소모적인 일이다.

나는 그래서 @NonNull을 사용하지 않고 Guava Preconditions로 null을 검증하고 오류 처리하는 것을 선호한다. Preconditions.checkNotNull은 branch를 만들지 않는다.

이 경우 에러 메시지를 명확하게 작성하는게 가능해지는 또 다른 장점도 생긴다.

// null일 경우 "userName must not be null." 메시지로 예외 발생
Preconditions.checkNotNull(userName, "userName must not be null.")

@ToString, @EqualsAndHashCode 필드명 지정시 오타 문제

Lombok 1.18.0 부터 @ToString, @EqualsAndHashCode에 대해 필드, 혹은 메소드에 Include, Exclude 지정이 가능해졌다. 따라서 아래 문제가 모두 해소 된다.

@ToString  @EqualsAndHashCode에서는 파라미터로 특정 필드를 지정해서 처리 대상에 포함시키거(of)나 제외(exclude)시킬 수 있다.

헌데 문제는 이게 필드 이름을 String으로 지정한다는 점이다. 이로 인해 IDE 에서 필드명을 리팩토링할 때 올바로 반영이 안되거나, 아주 단순한 오타가 나도 눈치를 못 챌 수 있다. 보통 오타등으로 인해 잘못된 필드가 지정되면 Compile 시점에 warning이 출력된다. 하지만 warning일 뿐, error가 아닌지라 그마저도 모르고 넘어갈 가능성이 높다.

현재 버전에서는 이를 error로 격상시킬 방법이 없다. 컴파일러 옵션 -Werror를 주면 warning시에도 오류를 내며 컴파일을 멈추는데, 또 다른 문제는 lombok과 관계없는 다른 너무 많은 경우에 대해서 에러를 내버린다.(java 8 현재 -Xlint를 통한 옵션 미세 조정이 제대로 작동을 안함. javac 버그로 보임)

이 문제는 Jenkins Log Parser Plugin 으로 어느정도 해결 가능하다.

Lombok Field 지정이 올바른지 검사 에 관련 방법을 정리해두었다.

실무 프로젝트에서는 보수적으로 사용

개인 Toy 프로젝트가 아닌 실무 프로젝트에서는 가급적 @Getter, @Setter, @ToString 만 사용하고 그 외의 것들 사용은 자제하거나 매뉴얼을 잘 읽어보고서 보수적으로 사용한다.

@EqualsAndHashCode는 Immutable 필드만 명시적으로 지정하면서 자제해서 사용하거나 가급적 IDE 코드 제너레이션등을 통해 코드를 직접 만든다.

특히 Experimental 기능은 사용하지 말고, IDE 지원이 확실치 못하고 문법 파괴적인 val도 사용하지 않는 것이 좋을 것 같다.

lombok.config를 통해 애노테이션 사용금지 및 각종 설정

lombok 도 이렇게 일부 기능에 대해 사용금지가 필요하다고 느꼈는지, Configuration System lombok.config를 통해 다양한 설정이 가능하게 해두었다.

예를들어, 프로젝트 최상단 디렉토리에 lombok.config 파일을 만들고 다음과 같이 지정하면 @Data, @Value, val, @NonNull, @AllArgsConstructor, @RequiredArgsConstructor 등의 사용이 금지된다.

억지로 사용할 경우 컴파일 오류가 발생한다.

config.stopBubbling = true
lombok.data.flagUsage=error
lombok.value.flagUsage=error
lombok.val.flagUsage=error
lombok.var.flagUsage=error
lombok.nonNull.flagUsage=error
lombok.allArgsConstructor.flagUsage=error
lombok.requiredArgsConstructor.flagUsage=error
lombok.cleanup.flagUsage=error
lombok.sneakyThrows.flagUsage=error
lombok.synchronized.flagUsage=error
# experimental 전체 금지
lombok.experimental.flagUsage=error

# 기타 각종 사용해서는 안되는 기능들을 모두 나열할 것.

상세한 설정 옵션은 각 애노테이션 매뉴얼 페이지 하단에 자세히 나와 있다.

Lombok 라이브러리에서 제공하는 어노테이션 중에서 자주 사용되는 어노테이션 위주로 살펴보도록 하겠습니다.

접근자/설정자 자동 생성

제일 먼저 살펴볼 어노테이션은 @Getter와 @Setter 입니다. 아마 Lombok에서 가장 많이 사용되는 어노테이션일 텐데요. 예를 들어, xxx라는 필드에 선언하면 자동으로 getXxx()(boolean 타입인 경우, isXxx())와 setXxx() 메소드를 생성해줍니다.

@Getter @Setter
private String name;

위와 같이 특정 필드에 어노테이션을 붙여주면, 다음과 같이 자동으로 생성된 접근자와 설정자 메소드를 사용할 수 있어서 매우 편리합니다.

user.setName("홍길동");
String userName = user.getName();

또한, 필드 레벨이 아닌 클래스 레벨에 @Getter 또는 @Setter를 선언해줄 경우, 모든 필드에 접근자와 설정자가 자동으로 생성됩니다.

VO 클래스를 작성할 때 마다, 접근자와 설정자 메소드를 작성해주는게 참 번거로운 일이었는데, (특히, 수정할 때는 더욱이) Lombok을 쓰게되면 이런 노가다성 코딩에서 해방될 수 있습니다. :)

생성자 자동 생성

Lombok을 사용하면 생성자도 자동으로 생성할 수 있습니다. @NoArgsConstructor 어노테이션은 파라미터가 없는 기본 생성자를 생성해주고, @AllArgsConstructor 어노테이션은 모든 필드 값을 파라미터로 받는 생성자를 만들어줍니다. 마지막으로 @RequiredArgsConstructor 어노테이션은 final이나 @NonNull인 필드 값만 파라미터로 받는 생성자를 만들어줍니다.

@NoArgsConstructor
@RequiredArgsConstructor
@AllArgsConstructor
public class User {
  private Long id;
  @NonNull
  private String username;
  @NonNull
  private String password;
  private int[] scores;
}
 
User user1 = new User();
User user2 = new User("dale", "1234");
User user3 = new User(1L, "dale", "1234", null);

ToString 메소드 자동 생성

toString() 메소드를 작성하는 것도 여간 귀찮은 일이 아닙니다. 하지만 Lombok을 사용하면 @ToString 어노테이션만 클래스에 붙여주면 자동으로 생성해줍니다.

예제와 같이 exclude 속성을 사용하면, 특정 필드를 toString() 결과에서 제외시킬 수도 있습니다.

 
@ToString(exclude = "password")
public class User {
  private Long id;
  private String username;
  private String password;
  private int[] scores;
}

위와 같이 클래스에 @ToString 어노테이션을 붙이고, 아래와 같이 필드를 세팅 후 출력을 하면,

User user = new User();
user.setId(1L);
user.setUsername("dale");
user.setUsername("1234");
user.setScores(new int[]{80, 70, 100});
System.out.println(user);

다음과 같이, 클래스명(필드1명=필드1값,필드2명=필드2값,...) 식으로 출력됩니다.

User(id=1, username=1234, scores=[80, 70, 100])

Lombok을 사용하기 전에는 Apache Commons Lang 라이브러리의 ToStringBuilder를 사용했었는데, Lombok을 사용하는 게 어노테이션 한 방으로 끝나니 훨씬 편한 것 같습니다.

equals, hashCode 자동 생성

자바 빈을 만들 때 equals와 hashCode 메소드를 자주 오버라이딩 하는데요. @EqualsAndHashCode 어노테이션을 사용하면 자동으로 이 메소드를 생성할 수 있습니다.

@EqualsAndHashCode(callSuper = true)
public class User extends Domain {
  private String username;
  private String password;
}
 

callSuper 속성을 통해 equals와 hashCode 메소드 자동 생성 시 부모 클래스의 필드까지 감안할지 안 할지에 대해서 설정할 수 있습니다.

즉, callSuper = true로 설정하면 부모 클래스 필드 값들도 동일한지 체크하며, callSuper = false로 설정(기본값)하면 자신 클래스의 필드 값들만 고려합니다.

User user1 = new User();
user1.setId(1L);
user1.setUsername("user");
user1.setPassword("pass");

User user2 = new User();
user1.setId(2L); // 부모 클래스의 필드가 다름
user2.setUsername("user");
user2.setPassword("pass");

user1.equals(user2);
// callSuper = true 이면 false, callSuper = false 이면 true

끝판왕! @Data

@Data는 위에서 설명드린 @Getter, @Setter, @RequiredArgsConstructor, @ToString, @EqualsAndHashCode을 한꺼번에 설정해주는 매우 유용한 어노테이션입니다.

@Data
public class User {
  // ...
}

사용 방법은 다른 어노테이션들과 대동소이합니다. 클래스 레벨에서 @Data 어노테이션을 붙여주면, 모든 필드를 대상으로 접근자와 설정자가 자동으로 생성되고, final 또는 @NonNull 필드 값을 파라미터로 받는 생성자가 만들어지며, toStirng, equals, hashCode 메소드가 자동으로 만들어집니다.

이상으로 자주 사용되는 Lombok 어노테이션에 대해서 살펴보았습니다. 다음 포스팅에서는 많이 알려지지는 않았지만 알아두면 유용한 Lombok 어노테이션에 대해서 알아보겠습니다.

크로스도메인 이슈란?

Ajax 등을 통해 다른 도메인의 서버에 url(data)를 호출할 경우 XMLHttpRequest는 보안상의 이유로 자신과 동일한 도메인으로만 HTTP요청을 보내도록 제한하고 있어 에러가 발생한다.
내가 만든 웹서비스에서 사용하기 위한 rest api 서버를 무분별하게 다른 도메인에서 접근하여 사용하게 한다면 보안상 문제가 될 수 있기 때문에 제한하였지만 지속적으로 웹 애플리케이션을 개선하고 쉽게 개발하기 위해서는 이러한 request가 꼭 필요하였기에 그래서 XMLHttpRequest가 cross-domain을 요청할 수 있도록하는 방법이 고안되었다.
그것이 CORS 이다.

CORS란?

CORS(Cross-origin resource sharing)이란, 웹 페이지의 제한된 자원을 외부 도메인에서 접근을 허용해주는 메커니즘이다.

스프링에서 CORS 설정하기.

스프링 RESTful Service에서 CORS를 설정은 @CrossOrigin 어노테이션을 사용하여 간단히 해결 할 수 있다. RestController를 사용한 클래스 자체에 적용할 수 도 있고, 특정 REST API method에도 설정 가능하다.
또한, 특정 도메인만 접속을 허용할 수도 있다.

사용예 )

@CrossOrigin(origins = “허용주소:포트”)

예제코드 1)

  • WebMvcConfigurer를 통해 적용하는 방식.
package io.jmlim.corssample.corssample;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
      // 모든 uri에 대해 http://localhost:18080, http://localhost:8180 도메인은 접근을 허용한다.
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:18080","http://localhost:8180");

    }
}

예제코드 2)

  • @CrossOrigin 어노테이션을 통해 적용하는 방식.
package io.jmlim.corssample.corssample;

 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
//해당 컨트롤러의 모든 요청에 대한 접근 허용(아래 도메인 두개에 대해서만..)
@CrossOrigin(origins = {"http://localhost:18080", "http://localhost:8180" }) 
@RestController
public class CorssampleApplication {
 
	//아래와 같이 특정 메소드에만 적용할수도 있다.
    //@CrossOrigin(origins = {"http://localhost:18080", "http://localhost:8180" })
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
	
	@GetMapping("/my")
    public String my() {
        return "my";
    }
	
 
    public static void main(String[] args) {
        SpringApplication.run(CorssampleApplication.class, args);
    }
}

예제코드 호출하기

만약 이 코드가 실행되는 웹서버의 도메인이 http://localhost:18080과 http://localhost:8080이 아닐경우 fail이 발생한다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

    <script
           src="https://code.jquery.com/jquery-3.3.1.js"
            integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60="
            crossorigin="anonymous"></script>
    <script>
       // alert(1);
        $(function() {
			  // 만약 이 코드가 실행되는 웹서버의 도메인이 http://localhost:18080과 http://localhost:8080이 아닐경우 fail이 발생한다.
            $.ajax("http://localhost:8180/hello")
                .done(function(msg) {
                    alert(msg);
                }).fail(function() {
                    alert("fail");
                });
        });
    </script>
</head>
<body>

</body>
</html>

+ Recent posts