직전 포스트에서 ResponseEntity를 소개했다. 이번엔 '인터셉터'라는 놈으로 멋진 일을 하나 해주려고 한다. 바로 컨트롤러로부터 나오는 애들의 형식을 모두 통일해주는 것! 

 

"그냥 컨트롤러에서의 리턴타입을 전부 통일시켜주면 되는거 아닌가요?"

 

라고 할 수 있다. 컨트롤러의 메서드 하나하나마다 리턴타입을 통일시키고 헤더, 응답코드들을 다 세팅해주고 하면 물론 당연히 응답을 다 통일시켜줄 순 있지만, 개발자는 반복을 싫어한다. 만약 컨트롤러의 메서드가 여러 개 있다면 그들 하나하나마다 리턴타입을 ResponseEntity로 해주고..등의 작업을 해야 하는 것이며, 뭐 헤더를 수정한다거나 하면 이들을 모두 하나하나 다시 건드려줘야 한다.

 

"이걸 좀 더 편하게 해주자"가 지금 해볼 내용이다.


그럼 이런 일을 어떻게 해줄 수 있는가? 바로 인터셉터를 통해 가능하다.

 

인터셉터(Interceptor)

스프링이 제공하는 기술로, 컨트롤러의 핸들러(Handler)를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있게 해주는 놈이다. 좀 더 풀이하자면 클라이언트로부터 들어온 request라는 편지가 컨트롤러라는 마을의 핸들러라는 집에 도착하기 전에 낚아채서(말 그대로 인터셉트) 개발자가 원하는 작업을 해준 후 마저 핸들러의 집에 보낼 수 있게 해주는 녀석이라고  볼 수 있다. 반대로 핸들러로부터 출발한 response라는 편지를 낚아채 개발자가 원하는 작업을 한 후에 클라이언트에게 마저 보내게 할 수도 있는 것.

(※ 핸들러 : 사용자가 요청한 url에 따라 실행되어야 할 메서드)

 

즉, 특정 컨트롤러의 핸들러가 실행되기 전이나 후에 어떤 작업을 하고 싶을 때 이 인터셉터라는 걸 사용한다. 즉 이를 통해 컨트롤러들이 실행된 후의 객체들에 대해 공통된 작업을 해준다면 응답값 통일을 반복적인 작업없이 간편하게 할 수 있는 거다!

 

암튼 나만의 인터셉터를 만들고 싶다면, 스프링에서 제공하는 HandlerInterceptor라는 인터페이스를 구현하는 커스텀 클래스를 만들면 된다. HandlerInterceptor는 다음과 같은 3개의 메서드를 가진다.

 

메서드명 리턴 타입 설명 기본값
preHandle boolean - 컨트롤러가 호출되기 전에 실행됨

- 컨트롤러의 실행 이전에 처리해야 할 작업이 있을 때 또는 요청정보를 가공할 때 등에 씀

- 실행되어야 할 '핸들러'에 대한 정보를 파라미터로 받음!

- true를 리턴하면 preHandle실행 후 핸들러에 접근하나 false를 리턴하면 작업을 중단함
true
postHandle void - 핸들러 실행 완료 후 View 생성 전에 실행됨

- ModelAndView 타입의 정보를 파라미터로 받음. 즉 Controller에서 View 정보를 전달하기 위해 작업한 Model의 정보를 참조하거나 조작할 수 있음!
-
afterCompletion void - View의 렌더링 이후 실행됨

- 요청 처리중에 사용한 리소스를 반환해주기 적당한 메서드
-

 

여기서 afterCompletion을 활용해 간편한 응답값 통일화 작업이 가능한 거다!

그 방법은 다음과 같다.


우선 다음과 같은 DTO클래스를 만들어준다.

 

@AllArgsConstructor
@Data
public class SuccessResponseDTO<T> {
    private final boolean success;
    private final T data;

    public SuccessResponseDTO(@Nullable T data) {
        this.success = true;
        this.data = data;
    }
}

 

클라이언트로 전달되는 모든 응답들은 요 ResonseDTO 형태로 나가도록 통일할 거다.

그리고 전에 만들어뒀던 필터의 doFilterInternal메서드에 코드들을 추가해줬다.

 

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    // 파라미터로 받은 request, response 객체들이 한 번 읽으면 없어지는 애들이라 한 번 감싼 애들을 만들어준다. 일종의 복제
    ContentCachingRequestWrapper wrappingRequest = new ContentCachingRequestWrapper(request);
    ContentCachingResponseWrapper wrappingResponse = new ContentCachingResponseWrapper(response);

    String token = tokenProvider.getTokenFromHeader(request);
    if (token != null && tokenProvider.validateToken(token, jwtSecretKey)) {
        Authentication authentication = tokenProvider.getAuthentication(token, jwtSecretKey);
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }
    // 필터엔 복제한 애들 넘기기. 때문에 인터셉터도 래핑된 객체를 파라미터로 받음
    filterChain.doFilter(wrappingRequest, wrappingResponse);
    // 마지막에 클라이언트에게 response나갈 때 wrapping한 내용을 써줌(복제본 내용을 원본에 쓴다!)
    wrappingResponse.copyBodyToResponse();
}

 

request, response를 감싼 객체들을 만들고 이 녀석들을 doFilter메서드에 인자로 넘기는 모습이다. (이전에는 request, response를 넘겼었음) 그리고 마지막에 캐싱한 응답값을 원래 응답에 덮어씌우는 모습.

 

우리가 하고 싶은 건 컨트롤러에서 뱉어진 응답을 인터셉터에서 가로채 afterCompletion() 메서드를 통해 통일된 응답값 형태로 만들고 싶은 것이니, 이때 필연적으로 인터셉터에서 HttpServletResponse의 Stream을 읽어야만 한다. 그런데 슬픈 사실은 HttpServletRequest, HttpServletResponse은 한 번씩만 읽을 수 있도록 구현된 애들이라는 거..(개발하신 분들이 이렇게 만들었다고) 근데  스프링이 Converter를 이용해 response의 내용을 json으로 바꿔주는 과정에서도 HttpServletResponse를 읽게 되니까 총 2번을 읽게 되어 오류가 생긴다는 거다.

 

이런 말도 안되는 문제를 해결하기 위해 wrapper클래스를 만들어 이 놈들의 내용을 저장해놓은 후 계속해서 쓰는 방식을 이용하는 것.

 

이 때 response를 wrapping한 객체는 마지막에 copyBodyToResponse()메서드를 꼭 불러줘야 한다. 실제로 클라이언트에게 보낼 response에 자신(wrapping한 객체)의 버퍼에 있는 내용을 덮어씌우는 메서드라고 함.

 

그리고 다음과 같은 커스텀 인터셉터를 만들어줬다.

 

@RequiredArgsConstructor
@Component
public class MyInterceptor implements HandlerInterceptor {
    private final String SUCCESS_PREFIX = "2";
    private final String JSON_CONTENT_TYPE = "application/json";
    // response를 object로 매핑해야 됨
    private final ObjectMapper objectMapper;

    @Override
    public void afterCompletion(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler, Exception ex
    ) throws Exception {
        // 필터에서 wrapping됐던 response가 산 넘고 강 건너 결국 이 메서드로 넘어올 것임
        final ContentCachingResponseWrapper cachingResponse = (ContentCachingResponseWrapper) response;
        // 200번대 응답이 아니면 인터셉터를 거치지 않도록
        if (!isSuccessStatus(response.getStatus())) {
            return;
        }
        if (cachingResponse.getContentType() != null
                && cachingResponse.getContentType().contains(JSON_CONTENT_TYPE)
                && cachingResponse.getContentAsByteArray().length != 0) {
            // String 변환
            String body = new String(cachingResponse.getContentAsByteArray());
            // Object 형식으로 변환 (Response에 꽂아주기 위함)
            Object data = objectMapper.readValue(body, Object.class);
            // 컨트롤러가 뱉는 모든 DTO들을 형식에 상관없이 ResponseDTO에 담는 것!
            SuccessResponseDTO<Object> objectSuccessResponseDTO = new SuccessResponseDTO<>(data);
            // String 변환
            String wrappedBody = objectMapper.writeValueAsString(objectSuccessResponseDTO);
            // 비우고 (원래는 여기에 SuccessResponseDTO가 아닌 다른 놈, 즉 통일된 형식을 갖추기 전의 무언가가 있었을 거니까)
            cachingResponse.resetBuffer();
            // 웅답값 교체
            cachingResponse.getOutputStream().write(wrappedBody.getBytes(), 0, wrappedBody.getBytes().length);
        }

    }

    private boolean isSuccessStatus(int status) {
        return String.valueOf(status).startsWith(SUCCESS_PREFIX);
    }
}

 

※ ObjectMapper : JSON을 Java객체로 역직렬화하거나 Java 객체를 JSON으로 직렬화할 때 자주 쓰이는 클래스(from Jackson 라이브러리). 그 외에도 Java객체의 내용물을 확인하거나 파싱하는데에 쓰임.

 

생성자가 한개라면 @Autowired 어노테이션을 생략가능한 점 + @RequiredArgsConstructor를 통해서 objectMapper를 주입받는 모습인데, 나는 ObjectMapper를 빈으로 등록한 적이 없다! 근데 왜 되는건 궁금해서 검색해보니까, 스프링 부트가 기본적으로 빈으로 등록한 ObjectMapper가 있다고 한다. JacksonAutoConfiguration을 뜯어보니까 요로코롬 자동으로 등록된 놈이 있는 걸 볼 수 있었음.

 

@AutoConfiguration
@ConditionalOnClass(ObjectMapper.class)
public class JacksonAutoConfiguration {

    // 생략
    
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
	static class JacksonObjectMapperConfiguration {

		@Bean
		@Primary
		@ConditionalOnMissingBean
		ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
			return builder.createXmlMapper(false).build();
		}

	}

    // 생략

}

 

암튼..

이렇게 내가 만든 커스텀 인터셉터를 다음과 같이 등록해줬다.

 

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
    private final MyInterceptor myInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(myInterceptor)
                .addPathPatterns("/**");
    }
}

 

  • addInterceptor : 내가 만든 인터셉터를 등록하는 메서드
  • addPathPatterns : 이 인터셉터를 호출할 경로 추가
  • excludePathPatterns : 이 인터셉터를 호출하지 않을 경로 설정

 

이제 끝났다.

내가 이해한 대로 동작순서를 정리하면 결국 다음과 같다.

 

  1. 클라이언트가 쏜 요청이 디스패처 서블릿에 도착하기 전에 필터를 만나게 됨.
  2. 필터에서 request, response를 래핑한 객체를 만들어서 얘를 디스패처 서블릿으로 보냄
  3. 디스패처 서블릿은 얘를 이러쿵저러쿵해서 컨트롤러한테 보냄
  4. 컨트롤러의 핸들러(본 케이스에서는 login메서드)에 의해 응답(본 케이스에서는 LoginResponseDTO)가 뱉어짐. 즉 얘가 response래핑한 객체에 담김
  5. 인터셉터에서 얘를 낚아서 SuccessResponseDTO로 형식 맞춰주고 이걸 response래핑한 객체에 갈아끼워넣음
  6. 필터에서 response래핑한 객체에 있는 내용물을 실제 response로 덮어씌워줌

 

그래서 한번 요청을 보내본 결과는..

 

 

응답값이 SuccessResponseDTO에서 맞춰준대로 통일된 모습이다.

 

이런식으로 개발한다면, 응답값을 만들어주는 작업을 컨트롤러 단에서 할 필요가 없다. 단순히 DTO만 반환시키면 된다! 반환한 DTO가 인터셉터 단에서 SuccessResponseDTO의 data로 꽂아지고, response를 갈아끼워서 응답을 내보내게 되기 때문. 이렇게해서 응답값이 통일이 된다.


소소한 실험

필터에서 request와 response를 래핑하는데, 사실 request까지 래핑할 필요가 있을까? 란 생각이 들었다. response는 두 번 읽게 되는데 request는 그렇지 않을 것 같았기 때문.

 

그래서 request는 래핑하지 말고 실험해봤다.

 

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    ContentCachingResponseWrapper wrappingResponse = new ContentCachingResponseWrapper(response);

    String token = tokenProvider.getTokenFromHeader(request);
    if (token != null && tokenProvider.validateToken(token, jwtSecretKey)) {
        Authentication authentication = tokenProvider.getAuthentication(token, jwtSecretKey);
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }
   
    filterChain.doFilter(request, wrappingResponse);
    wrappingResponse.copyBodyToResponse();
}

 

결과는 성공이었따..!

 


참고한 글들입니다.

https://popo015.tistory.com/115

 

[Spring] 스프링 인터셉터(Interceptor)란 ?

목표 Interceptor 란 무엇인지 알아본다. Interceptor 를 직접 구현해본다. 순서 1. 인터셉터(Interceptor) 1.1 인터셉터란? 1.2 왜 사용하는가? 1.3 구현수단 1.4 어떤 메서드를 가지고 있는가? 2. 인터셉터 동작

popo015.tistory.com

https://kihwan95.tistory.com/7

 

스프링 인터셉터란(Interceptor)란? 인터셉터 적용해보기

스프링 인터셉터란 사전적 의미로 가로채다는 의미가 있습니다. 이걸 스프링에 적목 시키면 인터셉터는 Controller에 들어오는 요청(HttpRequest), 응답(HttpResponse)를 가로채는 역할을 하는 객체입니다

kihwan95.tistory.com

https://kimvampa.tistory.com/127

 

[spring] Spring Interceptor 란?(HandlerInterceptor, HandlerInterceptorAdapter)

목표 Interceptor 란 무엇인지 알아본다. Interceptor를 직접 구현해본다. 순서 1. 인터셉터(Interceptor) 1.1 인터셉터란? 1.2 왜 사용하는가? 1.3 구현 수단 1.4 어떤 메서드를 가지고 있는가? 2. 인터셉터 동작

kimvampa.tistory.com

 

스터디를 진행하던 중 멘토님이 Lombok라이브러리를 사용해서 스터디를 진행하셨다. 하지만 스프링에 막 발을 들인 나는 롬복이 뭔지 모른다. 그래서 정리해봤다.


롬복?

어노테이션 기반으로 코드를 자동완성해주는 라이브러리라고 한다. 개발자 편의를 위해 쓰는 라이브러리인 듯 하다. 다른 언어도 마찬가지지만 자바 언어 역시 기계적으로 작성해야 하는 코드들이 상당히 많이 생기는데 그런 부분들을 자동화해주는 라이브러뤼. 사용한다면 귀찮은 부분들을 작성해주는 걸 편하게 할 수 있을 뿐더러 코드의 길이 자체가 줄어드는 효과를 얻는다는 이점이 있다.

 

 

활용예시 - Getter, Setter

클래스를 만들었다. 이 놈이 갖는 필드 하나하나에 대해 getter, setter를 하나하나 만들어줘야 하는데 여간 귀찮은 게 아니다. 이럴 때 롬복을 이용해 Getter, Setter어노테이션을 주면 지가 알아서 필드들에 대한 getter, setter들을 만들어준다.

 

@Getter
@Setter
public class Member {
    private Long id;
    private String name;
}

 

참고로 클래스 이름 위에 이 어노테이션을 작성하면 모든 필드들에 대해 적용되고, 필드 이름 위에 작성하면 해당 필드에만 적용된다.

 

 

활용예시 - NoArgsConstructor

빈 기본 생성자를 만들어준다.

 

@NoArgsConstructor
public class Member {
    private Long id;
    private String name;
    
   /* 이걸 자동으로 만들어준다
   public Member() {
   }
   */
}

 

 

활용예시 - AllArgsConstructor

모든 필드에 대한 생성자를 만들어준다. (DI(Dependency Injection)를 해주는 식으로)

 

@AllArgsConstructor
public class Member {
    private Long id;
    private String name;
    
   /* 이걸 자동으로 만들어준다
   public Member(Long id, String name) {
        this.id = id;
        this.name = name;
   }
   */
}

 

 

활용예시 - RequiredArgsConstructor

특정 필드들에 대한 생성자를 만들어준다. 도대체 어떤 필드들에 대해 해주냐? final이 붙은 필드들, 그리고 @NonNull 어노테이션이 붙은 필드들에 대해서 자동으로 생성자를 만들어준다. AllArgsConstructor와 마찬가지로 DI(Dependency Injection)을 해주는 생성자를 만들어준다. 

 

※ NonNull 어노테이션 : 롬복에서 쓰는 어노테이션으로, 얘가 멕여진 필드가 null이 되면 NullPointerException을 일으킨다.

 

@RequiredArgsConstructor
public class Member {
    private final Long id;
    @NonNull
    private String name;
    private int age;
    
   /* 이걸 자동으로 만들어준다
   public Member(Long id, String name) {
        this.id = id;
        this.name = name;
   }
   */
}

 

 

이 외에도 어노테이션들이 많다. 클래스에 대한 equals함수와 hashCode함수를 자동으로 만들어주는 @EqualsAndHashCode, 필드들을 기반으로 ToString메서드를 자동으로 만들어주는 @ToString, 객체 생성에 Builder패턴을 적용해주는 @Builder.. 암튼 다양하다. 그때그때 찾아가며 공부하면 될 듯.


스프링에서 @RequiredArgsConstructor를 사용한 생성자 주입

기존에 내가 알고 있던(물론 스프링 공부 시작한지 별로 안됨) 생성자 주입 방식은 이거였다.

 

@Service
public class MemberService {
    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

 

(참고로 memberRepository같은 빈들은 스프링 컨테이너가 관리해준다고 하는데 얘네가 싱글톤 객체라..변하지 않기 때문에 final키워드를 붙이는 게 좋다고 한다)

 

여기서 잠깐 살펴볼 것이, 스프링은 생성자가 1개뿐이라면 @Autowired를 생략해도 된다. 이 경우 @Autowired를 자동으로 인식해 처리하기 때문. 즉 위 코드에서 @Autowired를 빼도 문제가 없다. 

 

그리고 @RequiredArgsConstructor는 final이 붙은 필드들에 대한 생성자를 알아서 만들어준다! 따라서 위 코드를 다음과 같이 아주 편하게 간소화시킬 수 있다.

 

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
}

 

원리는 간단하다.

 

  • RequiredArgsConstructor가 생성자를 만들어줌
  • 그 생성자에 대한 Autowired를 자동으로 인식해 처리함

스프링으로 DB의 데이터에 접근하는 방법은 크게 다음과 같이 4가지 방식이 있다.

 

  1. jdbc
  2. jdbcTemplate
  3. JPA
  4. 스프링 데이터 JPA

 

각각이 뭔지 간단하게만 알아보자.


1. Jdbc

Java Database Connectivity의 줄임말로, 자바 언어를 통해 DB와 연동하여 데이터를 주고받을 수 있게 해주는 API이다. 자바 언어로 작성된 응용프로그램과 DB의 중간에서 서로간의 통신을 번역해주는 녀석이라고 생각할 수 있다. 

 

기존의 DB접근 방법은 TCP/IP를 통해 서버와 DB가 서로 커넥션을 연결하고, SQL을 DB에게 전달하면 DB가 전달받은 SQL을 수행하고 그 결과를 응답하는 방식이었는데 문제는 각 DB들마다 저마다의 사용방법과 접근방법이 조금씩 다르단 것이었다! 즉 DB를 바꿀 때마다 그에 맞춰 접근하는 코드도 함께 바꿔야하는 큰 번거로움이 있었고, 이를 해결하고자 DB접근 표준 인터페이스인 Jdbc가 등장한 것이다.

 

그러나 단점으로는 상당히 복잡한 코드를 짜야 돼서 고생을 많이 한다는 거고, sql 쿼리는 직접 짜야 한다. 

 

DB와 커넥션을 맺고, SQL을 담은 내용(Statement)을 보내고, 그에 대한 응답(ResultSet)을 받고, 커넥션을 끊는 4단계로 동작한다.

 

2. JdbcTemplate

이 놈은 실무에서도 많이 쓴다고 한다. 순수 jdbc를 좀 더 효율적으로 사용할 수 있게끔 해주는 틀로, 스프링 프레임워크에서 제공해주는 놈이다. 순수 Jdbc의 단점인 "복잡한 코드 짜야 함!"을 개선하여, Jdbc에서 흔히 마주치는 반복 코드를 대부분 제거한다. 그러나 SQL쿼리는 직접 짜야 한다는 단점이 존재하긴 한다. 요약하자면 jdbc로 좀 더 간결한 코드를 짤 수 있게끔 해준다.

 

3. JPA

Java Persistence API의 줄임말이다. 자바 진영의 ORM기술 표준 인터페이스이고, 이를 여러 업체가 구현체로 만들었으며 대표적으론 hibernate 등이 있다. JdbcTemplate가 해냈던 기존의 반복 코드 제거는 물론 기본적인 SQL도 JPA가 지가 알아서 만들어서 실행해준다. 이를 통해 개발자가 SQL에 초점을 맞추지 않고 객체 중심의 설계에 좀 더 초점을 맞추게 된다는 점에서 개발 생산성이 크게 높아지게 된다.

 

※ ORM : Object Relational Mapping의 약자로 객체와 관계형 DB를 매핑하는 것을 말함

 

4. 스프링 데이터 JPA

스프링에서 jpa를 편하게 쓸 수 있도록 지원해주는 기술이다. save, findAll, delete와 같이 간단한 CRUD기능들을 공통으로 처리하는 JpaRepository를 제공한다. 인터페이스 메서드만 선언하면 메서드 이름만으로 쿼리를 생성하는 마법같은 기능도 제공한다. (예를 들어 findByName으로 메서드를 지으면 지가 알아서 이름으로 찾는 쿼리를 작성해줌. 물론 이를 위해서 당연히 정해진 규칙에 따라 메서드 이름을 작성해야 함. 자동으로 내부에서 구현체를 만들어 동작시키는 것임)  

 

 

 

스프링에서 사용할 객체를 위해 클래스를 만들어줬다고 해도 그 녀석을 사용하려면 스프링 빈으로 등록을 해야 한다. 만약 빈으로 등록하지 않으면, 스프링에서 사용할 수 없게 된다. 근데 빈이란 것이 도대체 무엇일까?

 

스프링 빈(Bean)

스프링 컨테이너가 관리하는 Java객체. 즉 스프링에 의해 생성되고 관리되는 Java 객체라고 보면 된다. 스프링 컨테이너가 일종의 바구니 같은 느낌이라면, 빈은 그 바구니에 담기는 콩들이라고 생각할 수 있다. 한마디로 이 콩으로 등록을 해야 스프링에서 이용가능하게 된다는 것..! 원래는 개발자가 직접 코드를 통해 new연산자나 팩토리 패턴 등으로 객체를 생성하고 소멸시키는데, 스프링은 컨테이너라는 바구니가 이 역할들을 대신해준다고 생각할 수 있다.

 

예를 들어 내가 MemberController를 만들고, 이 컨트롤러가 MemberService를 통해 회원가입이나 회원조회를 할 수 있게끔 만들려고 한다면 내가 만든 MemberController와 MemberService를 빈으로 등록을 해야 스프링에서 이용 가능하다. 이 때 다음과 같은 2가지 방법으로 스프링 빈 등록이 가능하다.

 

1. 어노테이션을 통한 등록

빈으로 등록할 클래스에 @Component라는 어노테이션을 멕이면 된다. 이렇게 해두면 스프링으 컴포넌트 스캔을 통해 지가 알아서 Bean으로 등록해준다. @Controller, @Service, @Repository등과 같이 @Component를 포함하는 어노테이션들도 컴포넌트 스캔을 통해 Bean으로 등록된다. 근데 그렇다고 아무데서나 저런 어노테이션 멕인다고 죄다 Bean으로 등록되는건 아니고, 컴포넌트 스캔을 하는 범위가 따로 있다. 기본적으론 @ComponentScan 어노테이션이 있는 파일의 패키지 아래를 모두 스캔하며, @SpringBootApplication(scanBasePackages ={{경로설정}}) 옵션으로 컴포넌트 스캔 시작 범위를 직접 설정할 수 있다. (이 경우 설정한 경로 기준으로 그 하위 애들만 스캔)  

 

암튼 어노테이션을 통한 등록법의 예시는 다음과 같다.

 

@Controller
public class MemberController {
    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}

 

@Autowired : 이 어노테이션이 보시다시피 생성자에 있으면 스프링에 컨테이너에 있는 memberService 빈을 찾아서 자동으로 연결해주게 한다(즉 이를 위해서 MemberService가 당연히 빈으로 등록돼있어야 한다). 이렇게 의존관계를 갖는 객체를 외부에서 넣어주는 이런 모습을 DI(Dependency Injection)이라고 한다. 여기서는 개발자가 직접 코드를 통해 주입하는 게 아니라 @Autowired 어노테이션에 의해 스프링에 알아서 주입해준다.

 

2. 자바 코드로 직접 등록

하나하나 직접 Bean으로 등록하는 방식이다. ~~Config라는 자바 파일을 만들고 거기에 다음과 같이 빈을 등록한다.

 

@Configuration
public class SpringConfig {
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

 

참고로 MemberService가 MemberRepository에 의존하기 때문에 MemberService의 경우 생성자에서 memberRepository를 넣어주는 모습.

 

@Bean이란 어노테이션을 보고 스프링이 이 객체들을 Bean으로 등록해준다. 

 

 

두 방법 중 그런 어떤 방법이 더 좋은 것일까?

 

상황에 따라 다르다.

 

주로 정형화된 컨트롤러나 서비스, 리포지토리 같은 경우는 어노테이션을 통한 등록이 편하다.

그러나 정형화되지 않았거나 상황에 따라 구현 클래스를 변경해야 하는 경우 등엔 코드로 직접 등록하는 게 더 편할 것이다. 왜냐하면 어노테이션을 통한 등록의 경우 변경할 때마다 새로 어노테이션을 작성해줘야 하는 번거로움이 있지만 직접 등록하는 경우는 1번만 바꾸면 되기 때문.

스프링 웹 개발은 다음과 같은 3가지 방법이 있다.

 

  1. 정적 컨텐츠 : 서버에서 특별히 뭔가 하는 거 없이 단순히 html파일을 클라이언트 쪽으로 넘기는 방식이다. 
  2. MVC & 템플릿 엔진 : 서버에서 특정한 작업을 통해 html파일을 동적으로 만들어 넘기는 방식이다. JSP, PHP 등의 템플릿 엔진이 사용되며 이걸 하기 위해 MVC패턴이 도입된다.
  3. API : JSON 포맷 등으로 '데이터'를 클라이언트에 전달하는 방식이다. 서버 간의 통신에도 활용되며, 요즘은 React같은 애들한테 API로 데이터를 넘기고 그걸 통해 화면을 클라이언트가 그리는 식으로도 활용된다.

 

정적 컨텐츠

resources폴더의 static폴더에 있는 파일을 찾아 넘긴다. 다음 순서로 동작한다고 이해할 수 있다.

  1. url를 입력
  2. 내장된 톰캣이 요청을 받고 스프링 컨테이너에게 요청을 넘김
  3. 컨트롤러가 url에 매핑되는 메서드가 있는지 먼저 찾음
  4. 없으면 내부의 resources/static에서 찾아서 넘김

 

MVC & 템플릿 엔진

MVC는 Model, View, Controller로 각자의 역할을 구분해 개발하는 방식을 일컫는다. Model과 Controller는 비즈니스 로직과 내부 로직을 처리하는 역할을 맡고, View는 화면을 그리는 역할을 맡는다. 옛날엔 JSP를 통해서 View에서 Controller의 역할도 수행했는데 이를 model 1방식이라 부르기도 함.

 

@Controller
public class HelloController {
    @GetMapping("hello")
    public String hello(Model model) {
        model.addAttribute("data", "hello!!");
        return "hello";
    }
}

 

  • GetMapping : GET POST 할 때의 get이며, /hello로 get요청이 들어왔을 때 hello라는 메서드를 실행한다는 거다. 
  • Model : View를 그리기 위해 사용되는 데이터들의 박스(?)로, key-value형태로 데이터들을 저장한다. 내부적으론 Map이 사용된다고 함.
  • return "hello" : 데이터들을 보낼 View이름을 지정하는 거라고 보면 된다.

 

 

이 방식의 동작순서는 다음과 같다.

 

  1. url를 입력
  2. 내장된 톰캣이 요청을 받고 스프링 컨테이너에게 요청을 넘김
  3. 컨트롤러가 url에 매핑되는 메서드가 있는지 먼저 찾음
  4. 있으면 해당 메서드를 호출
  5. viewResolver가 지정된 이름의 View를 찾아주고 템플릿 엔진과 연결시켜줌
  6. 템플릿엔진이 렌더링한 html파일을 반환

 

API

이 방식은 html파일이 아니라 데이터를 넘기는 방식이라고 볼 수 있다. 결국 정적 컨텐츠를 넘기는게 아니라면

 

  • html로 내려주든가
  • api로 (즉 데이터로) 주든가

 

둘 중에 하나인 거다.

 

@Controller
public class HelloController {
    @GetMapping("hello-string")
    @ResponseBody
    public String helloString(@RequestParam("name") String name) {
        return "hello" + name;
    }
}
  • GetMapping : GET POST 할 때의 get이며, /hello-string로 get요청이 들어왔을 때 helloString라는 메서드를 실행한다는 거다. 
  • @ResponseBody : 이 어노테이션을 쓰면 viewResolver를 쓰지 않고, 대신에 Http Response의 BODY에 데이터를 담게 된다.
  • @RequestParam("name") String name : 쿼리에서 "name"에 해당하는 값을 name이란 파라미터로 받아온다는 거다.
  • return "hello"  + name : 이 문자열 자체를 데이터로 준다는거다.

 

참고로 객체를 반환하면 객체가 JSON으로 변환되서 보내진다!

@Controller
public class HelloController {
    @GetMapping("hello-api")
    @ResponseBody
    public Hello helloApi(@RequestParam("name") String name) {
        Hello hello = new Hello();
        hello.setName(name);
        return hello;
    }

    static class Hello {
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}

이 방식의 동작순서는 다음과 같다.

 

  1. url를 입력
  2. 내장된 톰캣이 요청을 받고 스프링 컨테이너에게 요청을 넘김
  3. 컨트롤러가 url에 매핑되는 메서드가 있는지 먼저 찾음
  4. 있으면 해당 메서드를 호출
  5. 근데 @ResponseBody가 있으면 viewResolver에게 전달하지 않고 httpMessageConverter에게 전달함
  6. 문자는 그대로 보내고, 객체는 JSON으로 바꿔서 전달한다.

 

 

용어 정리

Gradle : 라이브러리들을 버전 설정하고 가져와주는 빌드 도구. 은 의존관계가 있는 라이브러리들도 함께 다운해준다(A라이브러리가 B에 의존하면 자동으로 B도 땡겨줌).

 

resources폴더 : 자바 파일(*.java)를 제외한 나머지 파일들을 위한 폴더

 

템플릿 엔진 : 지정된 템플릿 양식과 데이터를 합쳐 HTML 문서를 출력하는 SW. 예를 들어  그냥 a.html을 작성하기만 하면 서버에선 그냥 걔를 클라이언트쪽으로 넘기기만 하는데, 템플릿 엔진을 쓰면 html에서 루프를 쓴다던가 어떤 데이터값을 넣는다든가 할 수 있는 것이다. 

 

<html xmlns:th="http://wwww.thymeleaf.org">

 

html태그에 이렇게 작성해두면 해당 html파일에서 타임리프(템플릿 엔진) 문법을 사용 가능하다. 이를 활용해 서버에서 받아온 특정 데이터 값을 넣고 싶으면

 

<p th:text="'안녕하세요' + ${data}">안녕하세요.손님</p>

 

이런 식으로 하면 된다. 참고로 태그 사이엔 딱히 뭔가를 쓸 필욘 없음. 근데 템플릿 엔진을 거치지 않고 html파일을 열거나 할 때 태그 사이의 값이 나온다. 이미지 태그의 alt값과 비슷한 역할이라고 생각할 수 있겠다.

 

 

+ Recent posts