모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸린다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.
basePackages
: 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 모두 탐색한다.
basePackages = {"hello.core", "hello.service"} 이렇게 해서 여러 시작 위치를 지정할 수 있다.
basePackageClasses
: 지정한 클래스의 패키지를 탐색 시작 위로 지정한다.
만약 지정하지 않으면 @ComponentScan
이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것이다. 최근 스프링 부트도 이 방법을 기본으로 제공한다.
예를들어, 프로젝트가 다음과 같은 구조가 되어 있으면
com.hello
→ 프로젝트 시작 루트, 여기에 AppConfig 같은 메인 설정 정보를 두고 @ComponentScan
어노테이션을 붙이고 basePackages
지정은 생략한다.
이렇게 하면 com.hello
를 포함한 하위는 모두 자동으로 컴포넌트 스캔의 대상이 된다. 그리고 프로젝트 메인 설정 정보는 프로젝트를 대표하는 정보이기 때문에 프로젝트 시작 루트 위치에 두는 것이 좋다. 참고로 스프링 부트를 사용하면 스프링 부트의 대표 시작 정보인 @SpringBootApplication
를 이프로젝트 시작 루트 위치에 두는 것이 관례이다. 여기에 @ComponentScan
이 들어있다.
컴포넌트 스캔은 @Component
뿐만 아니라 다음과 같은 내용도 추가로 대상에 포함한다.
사실 어노테이션에는 상속관계라는 것이 없다. 그래서 이렇게 어노테이션이 특정 어노테이션을 들고 있는 것을 인식할수 있는것은 자바 언어가 지원하는 기능이 아니고 스프링이 지원하는 기능이다.
컴포넌트 스캔의 용도 뿐만 아니라 다음 어노테이션이 있으면 스프링은 부가 기능을 수행한다.
@Service
는 특별한 처리를 하지 않는다. 대신 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나라고 비즈니스 계층을 인식하는데 도움이 된다.이처럼 여러 비즈니스 로직에서 반복되는 부가 기능을 하나의 공통 로직으로 처리하도록 모듈화해 삽입하는 방식을 AOP라고 합니다.
이러한 AOP를 구하는 방법은 세가지가 있습니다.
1. 컴파일 과정에 삽입하는 방식
2. 바이트코드를 메모리에 로드하는 과정에 삽입하는 방식
3. 프록시 패턴을 이용한 방식
이 가운데 스프링은 디자인 패턴 중 하나인 프락시 패턴을 통해 AOP 기능을 제공하고 있습니다. 스프링 AOP의 목적은 OOP와 마찬가지로 모듈화해서 재사용 가능한 구성을 만드는 것이고, 모듈화된 객체를 편하게 적용할 수 있게 함으로써 개발자가 비즈니스 로직을 구현하는데만 집중할 수 있게 도와주는 것입니다.
private 메서드에 @Transactional을 선언하면 컴파일 에러가 발생합니다.
그렇다면 왜 사용할 수 없을까?
원인을 파악하려면 프록시
에대해 알아봐야합니다. 스프링 aop에서 프록시는 크게 JDK Dynamic proxy또는 CGLIB으로 작동합니다. 그리고 spring boot 1.4 버전 이후부터는 default로 CGLIB
을 사용합니다.
CGLIB은 동적으로 상속을 통해 프록시를 생성합니다. 따라서 private 메서드는 상속이 불가능하기 때문에 프록시로 만들어지지 않는것이죠!
그러면 아래처럼 protected나 public으로 메서드를 만들면 정상적으로 트랜잭션이 동작할까요? 정답은 protected일 때 또한 정상 동작하지 않습니다.
분명히 인텔리제이로 확인을 해보면 컴파일 에러는 나오지 않는데 말이죠. 해당 이유는 앞서 말했던 JDK Dynamic proxy
가 원인이었기 때문입니다. JDK Dynamic proxy는 인터페이스를 기반으로 동작합니다. 따라서 protected 메서드에서는 프록시가 동작할 수 없는 것이죠. 그래서 스프링에서는 일관된 AOP적용을 위해서 protected로 선언된 메서드 또한 트랜잭션이 걸리지 않도록 한 것입니다. 즉, 프록시 설정에 따라 트랜잭션이 적용되었다 안되었다 하는 변칙적인 결과를 막기 위함인거죠.
public class A {
public void init() {
this.progress();
}
@Transactional
public void progress() {
}
}
예를들어 다음과 같은 상황에서 progress()는 정상적으로 트랜잭션이 적용될까요?
이번에도 안됩니다.
Spring AOP에서 프록시의 동작 과정을 보면 프록시를 통해 들어오는 외부 메서드 호출을 인터셉트 하여 작동합니다. 바로 이러한 성격때문에 self-invocation이 라고 불리는 현상이 발생합니다.
프록시의 내부 빈에서 프록시를 호출 했다는 것이죠.
main메서드에서 그림과 같이 A의 init()를 호출하고 init()에서 progress()를 호출하면 그림과 같이 프록시 내부에서 호출하게 됩니다. 따라서 proxy가 인터셉트하지 못해서 트랜잭션이 동작하지 않는것입니다.
그렇다면 내부 메서드에서 호출은 AOP적용이 불가능하다. 이건 아닙니다. 해결 방법이 있습니다.
타겟 내에서 타겟의 다른 메서드를 호출할 때, 런타임에 실제 트랜잭션이 작동하지 않는다고 합니다. 즉, 런타임 시점에 작동은 안 하지만 이것을 컴파일 시점에 적용하면 된다는 것이죠.
그렇다면 어떻게 컴파일 시점에 프록시를 적용할 수 있을까요?
스프링에서 AOP를 구현하는 방식에는 스프링 AOP도 있지만 AspectJ라는 강력한 도구도 있습니다. AspectJ는 스프링AOP와 다르게 컴파일 시점에 위빙이 이루어집니다. 따라서 AspectJ를 사용하면 self-invocation문제를 해결할수 있는것이죠!
그런데 만약 Spring AOP를 쓰고있는 상황에서 이런 문제가 발생하면 어떡하죠? 이럴경우 다른 방법도 존재합니다!
내부에서 프록시를 호출하면 인터셉터가 작동하지 않으므로 외부에서 호출하는 방식으로 해결하는 것입니다.
- @Resource
- @Autowired
- @Inject
스프링 부트 2.6 버전부터는 기본적으로 순환 참조를 금지하도록 변경되었습니다. 만약 2.6 버전 이상에서 실습해보고 싶으시다면 아래의 설정을 추가하시면 됩니다.
spring: main: allow-circular-references: true
GET API
는 웹 애플리케이션 서버에서 값을 가져올 때 사용하는 API입니다.
실무에서는 HTTP 메서드에 따라 컨트롤러를 구분하지 않지만 여기서는 메서드별로 클래스를 생성하겠습니다.
package com.example.demo3.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/get-api")
public class GetController {
}
@RequestMapping 어노테이션을 별다른 설정 없이 선언하면 HTTP의 모든 요청을 받습니다. 그러나 Get 형식의 요청만 받기 위해서는 어노테이션에 별도의 설정이 필요합니다. RequestMethod.GET으로 설정하면 됩니다.
실무 환경에서는 매개변수를 받지 않는 메서드는 거의 쓰이지 않습니다. 웹 통신의 기본 목적은 데이터를 주고받는 것익 대문에 대부분 매개변수를 받는 메서드를 작성하게 됩니다. 매개 변수를 받을 때 자주 쓰이는 방법 중 하나는 URL 자체에 값을 담아 요청하는 것입니다.
// http://localhost:8080/api/v1/get-api/variable1/{String값}
@GetMapping(value = "/variable1/{variable}")
public String getVariable1(@PathVariable String variable) {
return variable;
}
// http://localhost:8080/api/v1/get-api/variable1/{String값}
이 URL을 보면 이 메서드는 중괄호로 표시된 위치의 값을 받아 요청하는 것을 알 수 있습니다.(실제 요청시 중괄호는 들어가지 않고 값만 존재합니다.) 값을 간단히 전달할 때 주로 사용하는 방법이며, GET 요청에서 많이 사용됩니다.
이러한 방식으로 코드를 작성할 때 몇 가지 지켜야할 규칙이 있습니다. @GetMapping 어노테이션 값으로 URL을 입력할 때 중괄호를 사용해 어느 위치에서 값을 받을지 지정해야 합니다. 또한 메서드의 매개변수와 그 값을 연결하기 위해 @PathVariable
을 명시하며, @GetMapping 어노테이션과 @PathVariable에 지정된 변수와 이름을 동일하게 맞춰야 합니다.
Controller의 요청에 맞추어 Repository에서 받은 정보를 가공하여 Controller에게 넘겨주는 비지니스 로직
흔히 얘기되는, MVC 패턴에서 비즈니스 로직 구성은 Controller, Service, Dao로 역할을 분산시켜 개발한다는 이야기는 일반적이다.
View
는 자신이 요청할 Controller만 알면 되며 Controller는 넘어온 매개변수를 이용해 Service
객체를 호출하기만 하면 된다. DAO
는 ibatis / Mybatis 등 데이터베이스 Connection을 통해 데이터를 주고 받는 역할이다.
Service는 POJO객체
로 구성된다. Controller처럼 Request / Response를 받지도 않고, DAO처럼 DB와 데이터를 주고받지도 않는다.
여기서 의문이 생긴다. 그러면 Service
가 왜 필요할까?
DAO는 단일 데이터 접근 로직이다. 말 그대로 SQL 하나 보내고 결과를 받는 것이 전부인 로직이다. 하지만 비즈니스 로직이 단순이 SQL 하나 보내서 끝나는 것이 아니다. 여러번의 DB 접근이 필요하고, 어떤 서비스는 병렬식으로 동시접근하여 데이터를 가져와야 하는 상황도 발생한다. 그렇기 떄문에 Service라는 개념이 나온 것이다. 하나의 서비스를 위해 여러개의 DAO를 묶은 트랜잭션이 생성되고, Service는 곧 트랜잭션의 단위가 된다. 또 다른 점으로, Controller 내부에서 필요한 여러 Service를 구분하는 필요성을 가진다. 비슷한 요청이더라도 내부 로직이 달라야한다면 Controller는 매우 복잡해질 가능성이 있다. 이러한 점을 분리하여 Controller는 단순이 요청을 받아 해당 요청에 맞는 Service에 데이터를 주입하는 역할이다.
재사용이라는 Spring의 큰 장점은 Service가 중요한 작용점이다.
Service를 이해하기 위해 큰 틀
1) Client가 Request를 보낸다.(Ajax, Axios, fetch등..)
2) Request URL에 알맞은 Controller가 수신 받는다. (@Controller , @RestController)
3) Controller 는 넘어온 요청을 처리하기 위해 Service 를 호출한다.
4) Service는 알맞은 정보를 가공하여 Controller에게 데이터를 넘긴다.
5) Controller 는 Service 의 결과물을 Client 에게 전달해준다.
우리가 흔히 상수를 정의할 때 final static string
과 같은 방식으로 상수를 정의를합니다. 하지만 이렇게 상수를 정의해서 코딩하는 경우 다양한 문제가 발생됩니다. 따라서 이러한 문제점들을 보완하기 위해 자바 1.5버전부터 새롭게 추가된 것이 바로 Enum
입니다. Enum은 열거형이라고 불리며, 서로 연관된 상수들의 집합을 의미합니다. 기존에 상수를 정의하는 방법이였던 final static string 과 같이 문자열이나 숫자들을 나타내는 기본자료형의 값을 enum을 이용해서 같은 효과를 낼 수 있습니다.
그렇다면 이것을 왜 사용할까?
코드가 단순해지며 가독성이 좋습니다.
인스턴스 생성과 상속을 방지하여 상수값의 타입 안정성이 보장됩니다.(컴파일 에러)
enum class를 사용해 새로운 상수들의 타입을 정의함으로 정의한 타입 이외의 타입을 가진 데이터값을 컴파일시 체크한다.
키워드 enum을 사용하기 때문에 구현의 의도가 열거임을 분명하게 알 수 있습니다.