Recording/멋쟁이사자처럼 BE 13기

[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_34일차_"스프링 Optional, Annotation"

LEFT 2025. 1. 20. 18:27

🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [34]일차

🚀34일차에는 필드를 통한 의존성 주입에 대한 공부와 Optional객체, Annotation 등에 대해 배울 수 있었다.

먼저 회고를 통해 이해가 어려웠던 생성자, 설정자 부분의 복습해야할 것 같다.

 


설정자를 통한 의존성 주입

 

UserExam 클래스

  • 사용자가 웹페이지에 접속하여 회원가입을 요청 (main)
// UserController를 주입받음
public class UserExam {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(UserConfig.class);
        UserController controller = context.getBean(UserController.class);
        controller.joinUser();
    }
}
  • new AnnotationConfigApplicationContext(UserConfig.class);
    ➡️UserConfig 클래스의 Bean들을 스캔하여 컨테이너에 등록

  • context.getBean(UserController.class);
    ➡️UserController Bean을 컨테이너로부터 가져옴.
    UserController는 UserService 클래스의 의존성이 주입된 상태로 반환

  • controller.joinUser();
    ➡️
    UserController의 joinUser()메소드로부터 UserService 인터페이스에 요청을 전달
    UserService인터페이스를 구현한 UserServiceImpl 구현체 클래스로 의존성이 주입되어 joinUser()의 정보가 전달됨

 

UserConfig 클래스

  • 모든 클래스와 의존성을 관리하는 컨테이너, 설정 클래스
  • 이 Spring컨테이너에서 모든 Bean이 등록되고 관리 (설정 클래스)
    = 모든 클래스와 의존성을 @Bean으로 설정하고 의존성 주입을 관리함
@Configuration
public class UserConfig {
    @Bean
    public UserDao userDao(){ // UserDao Bean으로 등록
        return new UserDaoImpl(); // UserDao 인터페이스의 구현체
    }

    @Bean
    public UserService userService() {
        UserServiceImpl userService = new UserServiceImpl();
        userService.setUserDao(userDao()); // UserDao 의존성 주입
        return userService;
    }

    @Bean
    public UserController userController() {
        UserController userController = new UserController();
        userController.setUserService(userService()); // UserService 의존성 주입
        return userController;
    }
}
  • UserService service = new UserServiceImpl();
    ➡️선언은 인터페이스타입으로, 생성은 구현체로한 것처럼
@Bean
public UserDao userDao(){
    return new UserDaoImpl();
}
  • Bean을 등록할때에도 타입은 인터페이스로 실제 생성은 구현체로 반환

 

UserConfig 클래스 - 생성자를 통한 주입방법

// 기존 설정자 주입방법
@Bean
public UserService userService() {
    UserServiceImpl userService = new UserServiceImpl();
    userService.setUserDao(userDao()); // 설정자로 UserDao 의존성 주입
    return userService;
}
@Bean
public UserController userController() {
    UserController userController = new UserController();
    userController.setUserService(userService()); // 설정자로 UserService 의존성 주입
    return userController;
}
//-----------------------------------------
// 바뀐 생성자 주입방법
@Bean
public UserService userService() {
    return new UserServiceImpl(userDao()); // 생성자 호출
}

@Bean
public UserController userController() {
    return new UserController(userService()); // 생성자 호출
}
  • setUserDao와 setUserService 메소드 호출 제거
  • 각 객체를 생성할때에 생성자를 통해 의존성을 주입하도록 바꿀 수 있음

UserController 클래스

  • 회원가입 요청을 접수받아서, 사용자의 요구사항에 따라 요청을 처리하는 서버로 전달 (서비스 계층 호출)
// UserService를 주입받음 (사용자의 요청을 처리하고 서비스 계층(UserService)에 전달)
public class UserController {
    // 유저컨트롤러를 요청하면 UserService에 의존하고 있을 것
    private UserService userService;

    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void joinUser(){
        User user = new User();
        user.setName("jones");
        user.setEmail("premier@league.com");
        user.setPassword("1111");

        userService.joinUser(user);
    }
}
  • UserController가 UserService에 의존하고 있다.
    (=UserController가 생성되기 위해서는 UserService가 먼저 생성되어야한다.)
  • 실제로 동작할때는 User정보를 사용자한테 받아올 것 (사용자가 정보를 주면서 회원가입해달라고 요청할 것)

 

UserController 클래스 - 생성자를 통한 주입방법

// 기존 설정자 주입방법
private UserService userService;
public void setUserService(UserService userService) {
    this.userService = userService;
}
//--------------------------------
// 바뀐 생성자 주입방법
private final UserService userService; // final 키워드 추가
public UserController(UserService userService) { 
    this.userService = userService;
}
  • setUserService 설정자 메소드 제거
  • 생성자에서 UserService의 의존성을 받도록 변경
  • final키워드 추가로 의존성이 바뀌지 않도록 함

 

UserService 인터페이스와 UserServiceImpl 구현체 클래스

  • 회원가입 요청을 처리하는 서버
    (데이터계층을 호출하여 데이터를 가져오거나 회원가입요청에 대한 응답을 수행)
public interface UserService {
    public void joinUser(User user);
}
// UserDao를 주입받음 (비즈니스 로직 수행 및 데이터 계층(UserDao) 호출)
public class UserServiceImpl implements UserService{
    private UserDao userDao;

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    // UserController 클래스 -> UserService 인터페이스로부터 주입받은 User객체를 처리
    @Override
    public void joinUser(User user) {
        userDao.addUser(user); // 유저를 넣어보내 저장하는 메소드
        // UserDao 인터페이스의 메소드 addUser()호출
        // UserDao인터페이스를 구현한 UserDaoImpl 구현체 클래스로 의존성이 주입되어 (>> addUser()의 정보가 전달됨)
    }
}

  • UserService가 UserDao에 의존하고 있다.
    (=UserService가 생성되기 위해서는 UserDao가 먼저 생성되어야한다.)
    ➡️userDao.addUser(user) 처럼 userDao가 있어야 메소드를 수행할 수 있음

UserServiceImpl 구현체 클래스 - 생성자를 통한 주입 방법

// 기존 설정자 주입방법
private UserDao userDao;
public void setUserDao(UserDao userDao) {
    this.userDao = userDao;
}
// -------------------------------
// 바뀐 생성자 주입방법
private final UserDao userDao; // final 키워드 추가 (의존성 불변)
public UserServiceImpl(UserDao userDao) { 
    this.userDao = userDao;
}
  • setUserDao 설정자 메소드를 제거하고 생성자에서 의존성을 받도록 함
  • userDao 필드에 final키워드를 추가하여 의존성 변경이 불가능하도록 함

UserDao 인터페이스와 UserDaoImpl 구현체 클래스

  • 데이터를 제공하거나 데이터를 저장하는 “창고” 개념
public interface UserDao {
    public User getUser(String email);
    public List<User> getUsers();
    public void addUser(User user);
}
// 주입받지 않고 직접 구현체로 사용 (데이터 저장 및 처리를 담당)
public class UserDaoImpl implements UserDao{
    @Override
    public User getUser(String email) {
        return null;
    }
    @Override
    public List<User> getUsers() {
        return null;
    }
    // UserDao 인터페이스의 addUser()메소드를 오버라이딩 (구현체)
    @Override
    public void addUser(User user) { // UserServiceImpl 구현체 클래스로부터 User객체를 전달받아 메시지 출력
        System.out.println(user.getName() + "의 정보가 성공적으로 저장되었습니다.");
    }
}

❓설정자 주입방식에서 생성자 주입방식으로 바꾸게되면서의 장점

➡️의존성을 강제할 수 있음
생성자에 의존성을 정의하여 의존성 없이는 객체를 생성할 수 없으므로
의존성을 필수적으로 주입하도록 할 수 있음


@ComponentScan

  • UserConfig에서 @Bean처럼 등록했던 부분을 @ComponentScan으로 바꿔 자동으로 Bean 등록
@ComponentScan(basePackages = "com.example.iocexam") // 어느 디렉토리를 스캔할 것인지 지정
public class UserConfig {
//...Bean 주석
}

 

// UserDao를 주입받음 (비즈니스 로직 수행 및 데이터 계층(UserDao) 호출)
@Service
public class UserServiceImpl implements UserService{
// 주입받지 않고 직접 구현체로 사용 (데이터 저장 및 처리를 담당)
@Repository
public class UserDaoImpl implements UserDao{
// UserService를 주입받음 (사용자의 요청을 처리하고 서비스 계층(UserService)에 전달)
@Controller
public class UserController {
  • 인터페이스가 아닌 구현체에 붙여준다.
    또한 이들은 @Component를 포함하기때문에 @ComponentScan의 대상에 해당된다.

@Autowired 

  • 오토 와이어드
  • 어노테이션이 붙은 것에는 자동으로 의존성 주입 가능
  • ComponentScan의 대상이 되는 것에 @Autowired를 지정
  • [생성자 @Autowired] : 만약 기본 생성자와 @Autowired 어노테이션의 생성자가 있을때
    @Autowired가 붙은 생성자를 먼저 스캔하게되고, 기본생성자는 자연스럽게 스캔 대상에서 제외된다.

  • [설정자 @Autowired] : 설정자를 통한 스캔도 마찬가지로 Setter메소드에 @Autowired를 붙여서
    명시적으로 스캔의 대상을 지정하고 실행할 수 있다.

  • @Repository는 어느 것도 주입받고 있지 않으므로 @Autowired로 스캔 대상을 지정할 필요가 없다.

  • 생성자, 설정자, 필드 주입방식에서 모두 사용 가능
  • 의존성 주입 우선 순위 : 필드 → 설정자 → 생성자

❓필드를 통한 의존성 주입을 권장하지 않는 이유

  • 스프링이 자동으로 수행하므로 스프링에 종속적일 수 있다.
  • 생성자 주입방법과 설정자 주입방법은 각각 생성자와 Setter설정자가 필요하므로
    스프링이 아니더라도 의존성 주입이 가능하지만 필드를 통해 의존성을 주입할때에는 스프링이 자동으로 수행
    ➡️따라서 스프링이 없으면 필드를 통한 의존성 주입 코드는 실행할 수 없을 것이므로
    스프링 없이도 실행 가능한 생성자, 설정자 방식과 달리 필드를 통한 주입 방식은 권장하지 않는 방법이다.

  • 생성자를 통해 객체 생성 시 의존성을 한 번에 주입받아 불변성을 확보할 수 있는 것과는 달리
    필드를 통한 주입은 객체의 안정성을 보장하기 어려움
    ➡️객체의 안정성을 보장하기 어려운 이유
    - 객체가 생성된 후에도 의존성이 변경될 수 있으므로
    - 의존성 주입을 위해 스프링 컨테이너가 필요하게 되므로 테스트가 어렵다.
    - 순환참조를 사전에 예방할 수 없음 (객체 A가 객체 B를 의존하는데 객체 B도 객체 A에 의존하는 구조)
@Service
public class ServiceB {

    @Autowired
    private ServiceA serviceA;

    public void run(){
        serviceA.run();
    }
}
  • @Autowired > private ServiceA serviceA
    ➡️필드를 통해 의존성을 주입하게 되면 ServiceA와 ServiceB가 서로 참조하게되는 순환참조 구조가 됨

  • 생성자를 통한 의존성 주입은 final 키워드 사용(=불변객체 사용)이 가능하다는 것과는 달리
    필드를 통한 의존성 주입은 final(=불변객체)로 고정할 수 없음
    ➡️객체가 생성되는 시점에 주입되지 않고 객체가 생성된 후에 주입이되므로
    final로 선언하게되면 객체 생성 시 상수로 값을 고정하므로 의존성을 주입할 수 없다.

  • 의존성을 숨길 수 없음
    ➡️필드를 통해 의존성을 주입할때 Reflection API를 사용
    생성자, 설정자 주입방식은 내부로직에서 의존관계를 주입(데이터만 넘겨받아 내부로직에서 의존관계 주입)

    필드 주입 방식은 Reflection API가 직접 필드에 접근해서 주입하게된다.
    ➡️외부 API에 의존성이 노출될 수 있음
    ➡️객체가 캡슐화되어 외부로부터 내부를 보호하는 객체지향프로그래밍 관점에서는 적합하지 않음

  • 테스트가 힘듦
    생성자, 설정자 주입방식은 테스트 객체를 매개변수로 넘길 수 있어 원하는 경우 테스트 객체 변경이 가능하지만
    필드주입방식은 @Autowired로 직접 필드에 객체를 주입하기 때문에 테스트 과정이 복잡합

❓설정자 주입방식과 생성자 주입방식의 상황 별 장점

➡️의존성 주입이 선택적인 경우에는 “설정자 주입 방식”이 유리
➡️의존성을 필수적으로 강제해야할때는 “생성자 주입 방식”이 유리

스프링에서는 생성자 주입 방식이 권장되지만 상황에 따라 설정자 주입도 유용
➡️생성자 주입방식이 권장되는 이유 : 의존성을 명확히하고, 테스트 가능성과 코드의 유지보수성을높일 수 있기때문


컨테이너 (Container)

  • 인스턴스의 생명주기를 관리
  • 생성된 인스턴스들에게 추가적인 기능을 제공
  • 중간에 끼어들어서 인스턴스 동작을 도와주기도 함
  • DI컨테이너 자체를 컨테이너라고 부르기도함. (객체 생성과 의존관계 설정을 담당)

DI (Dependency Injection)

  • 의존성 주입
  • 클래스 사이의 의존관계를 Bean 설정 정보를 바탕으로 컨테이너가 “자동으로 연결해주는 것”을 의미
  • DI 관련 용어 :
    ➡️Bean :
    - 스프링에서 DI를 사용하기 위해 생성되는 객체 (=DI 컨테이너가 관리하는 객체)
    - DI컨테이너가 빈을 생성하고, 초기화, 보관, 필요한 곳에 제공까지 담당
    - 스프링 컨테이너를 통한 관리의 자동화로 개발자가 복잡한 객체 생성 및 관리 과정에 관여하지 않아도됨
    - 기본적으로 싱글턴패턴이어서 하나의 인스턴스만 생성되어
    어플리케이션 내에서 해당 Bean에 대한 요청이 있을때마다 동일한 객체 인스턴스가 반환

    ➡️BeanFactory : 스프링에서 Bean을 생성하고 관리하는 컨테이너

    ➡️@Component : 스프링에서 Bean을 생성하기위한 어노테이션 중 하나로 해당 클래스를 Bean으로 등록하는 역할

    ➡️ApplicationContext : BeanFactory를 상속한 스프링 컨테이너로
    단순한 기능을 제공하는 BeanFactory를 확장하여 더 다양한 기능을 제공

    ➡️Autowiring(=자동주입) : 자동으로 Bean을 주입하는 기능으로 @Autowired 어노테이션을 통해 사용

    ➡️Qualifier : 같은 타입의 Bean이 여러개 있을 경우 어떤 Bean을 사용할지 결정하는 용도로 사용

    ➡️Configuration(=구성) : DI컨테이너가 객체를 생성하고 의존관계를 설정하기 위해 참조하는 설정정보

@SpringBootApplication

1. 기존 방법 (AnnotationConfigApplicationContext())

public static void main(String[] args) {
    // UserConfig 클래스의 Bean들을 스캔하여 컨테이너에 등록
    ApplicationContext context = new AnnotationConfigApplicationContext(UserConfig.class);
}
  • 기존에 UserConfig 객체를 가져와서 실행하던 방법 (AnnotationConfigApplicationContext 활용)

  • run()메소드는 ApplicationContext를 반환하므로

2. 두번째 방법 (SpringApplication.run())

@SpringBootApplication
public class IocexamApplication {
	public static void main(String[] args) {
		ApplicationContext context = SpringApplication.run(IocexamApplication.class, args);
		UserController controller = context.getBean(UserController.class);
		controller.joinUser();
	}
}
  • ApplicationContext에 넣어서 실행이 가능

Optional

  • null에 대한 처리를 하기 위한 자바에서 추가된 클래스 (객체)
  • 메소드가 반환할 결과 값이 '없음'을 명확할때 사용
  • NULL 반환 시 에러 발생 가능성이 높은 상황에서 메소드의 반환 타입으로 Optional을 사용
  • 스프링 4부터는 JDK 8부터 추가된 java.util.Optional 객체를 사용할 수 있음
Optional<String> optional = Optional.of("Hello");
if(optional.isPresent()){
	System.out.println(optional.get());
}
  • of("Hello") : 값이 NULL이 아닌 경우 사용(=값이 반드시 존재해야함), “Hello”값이 존재하는지 확인해서 처리, 
  • ofNullbale() : 값이 NULL일 수도 아닐 수도있는 경우 사용, 값이 NULL이면 빈 Optional객체 반환
  • orElse(”Hi”) : 값이 없다면 “Hi”라는 대체 값을 넣어주기도함
  • orElseThrow() : 값이 null이었을때 (값이 없다면) 예외 발생 시키기 (=오류 처리를 강제함)
  • isPresent() : optional 객체가 값을 가지고 있다면 true, 없다면 false
  • ifPresent() : 값이 존재할때 실행할 동작을 정의
  • isEmpty() : 값이 비어있다면 처리할 부분을 구현 가능

▶️실습 - 회원가입 서비스에 적용 : Optional 객체

UserDaoImpl 클래스

  • Optional반환타입으로 바꿀 메소드는 UserDao 인터페이스에서 먼저 Optional 반환타입으로 수정
// 기존 코드
@Repository
public class UserDaoImpl implements UserDao{
    //...
    @Override
    public List<User> getUsers() {
        return null;
    }
}
// Optional 사용
@Repository
public class UserDaoImpl implements UserDao{
    //...    
    @Override
    public Optional<User> getOptionalUsers() { // 실제 메소드에서는 DB 등에서 데이터를 꺼내옴
        User user = new User();
        return Optional.of(user);
    }
}
  • 리스트 반환타입을 가지는 getUsers()메소드를 Optional 반환타입을 가지는 getOptionalUsers()로 바꿔줄 수 있다.
  • Optional.of(user) : user값이 NULL이 아닌 경우 사용할 수 있는 메소드

 

UserServiceImpl 클래스

// 기존 코드
@Service
public class UserServiceImpl implements UserService{
    private UserDao userDao;

    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public void joinUser(User user) {
        userDao.addUser(user); 
    }**
}
// Optional 사용
@Service
public class UserServiceImpl implements UserService{
    // ... 기존 코드
    
    // Getter 추가
    public void getUser(String id){
    	User user = userDao.getOptionalUser().orElseThrow();
  	}
}
  • orElseThrow() : 값이 없으면 예외를 발생

정리하자면 명시적으로 null체크를 해준 것을 Optional로 간단히 표현이 가능하다는 것

userDao.getOptionalUser().orElse(new User());
  • Optional객체에서 값을 꺼내오지만 값이 존재하지 않을 경우에는 “새로운 기본 객체”를 반환
  • 즉 예외를 던지기보단 기본값을 제공하여 프로그램이 계속 실행되도록 보장하는 코드

❓Optional 사용의 장점

  • null 관련 문제 예방 (명시적으로 값의 유무를 처리함)
  • 코드 가독성 향상 (isPresent, orElse, map 등 사용)
  • 안전한 의존성 관리 (Optioanl을 반환타입으로 사용하여 호출자가 결과값에 안전하게 접근 가능)
  • 메소드 반환타입이 Optional이면 호출자가 값이 없을 “가능성을 미리 인지”할 수 있음

❓Optional 사용의 단점

➡️모든 메소드 반환값에 Optional을 사용할 경우 불필요한 복잡성 유발

❓Optioanl 사용 시기

➡️메소드 반환값으로 사용 (값이 없을 가능성이 있는 메소드의 반환타입으로 적합)


Dao객체가 여러 개일때 접근 방법

// 주입받지 않고 직접 구현체로 사용 (데이터 저장 및 처리를 담당)
@Repository
public class UserDaoImpl implements UserDao{
    // ...
}
@Repository
public class UserJuunbImpl implements UserDao{
    //...
}
  • 이처럼 UserDao가 2개가 구현되어있으면 오류가 발생
    ➡️해결방법 : 타입을 사용한 오토와이어링 방식

타입을 사용한 오토와이어링

  • Dao 객체의 ID를 명시적으로 지정하지 않으면 클래스 첫글자만 소문자로 바꾼 이름을 “ID”로 판단
  • ex. UserJuunbImpl 이면 "userJuunbImpl" id로 지정
    UserDaoImpl 이면 "userDaoImpl" id로 지정

  • ID를 지정하는 법 : ex. @Repository(”userDao”), @Repository(”userJuunb”)

  • Dao 객체를 사용하는 곳에서는
    ➡️@Qualifier(”userDao”) 처럼 기본생성자 안에서 어노테이션을 명시해야한다.

1. 생성자에서 @Qualifier 사용

// @Qualifier : 생성자에 특정 id의 Dao에게 의존성 주입을 해달라는 표기
public UserServiceImpl(@Qualifier("userDao") UserDao userDao) {
    this.userDao = userDao;
}
  • UserDao타입의 userDao를 가져올때
    UserJuunbImpl (UserDao타입)과 UserDaoImpl (UserDao타입) 2개가 존재하므로
    @Qualifier로 userDao의 ID를 지정하여 지정한 ID를 가진 UserDao타입을 가져올 수 있게한다.

  • 만약 public UserServiceImpl(){} 같은 기본 생성자가 공존하면 기본 생성자부터 수행하므로 오류가 발생할 수 있다.
    따라서 UserDao 타입을 받는 생성자 사용시 기본생성자를 주석처리하거나 없애주어야
    UserDao타입의 생성자에서 @Qualifier 을 사용하는 방식이 오류가 발생하지 않는다.

2. 설정자에서 @Qualifier 사용

@Autowired
public void setUserDao(@Qualifier("userDao") UserDao userDao) {
    this.userDao = userDao;
}
  • 생성자에 @Qualifier를 넣어준것처럼 설정자에 @Qulifier를 넣어줄때는@Autowired와 함께 써주어야한다.

 

// 주입받지 않고 직접 구현체로 사용 (데이터 저장 및 처리를 담당)
@Repository("userDao")
public class UserDaoImpl implements UserDao{ ... }
  • @Qualifier 어노테이션은 구현체 클래스의 @Repository 뒤에 명시한 ID를 가져온다.
    만약 명시하지 않았다면 자동으로 ID를 판단한다.(ex. ID = userDaoImpl (첫글자만 소문자로))

이름으로 오토와이어링

  • 자바 JSR-250 표준에서 사용하는 @Resource 어노테이션
    (이름 기반으로 Bean을 찾아 자동으로 의존성을 주입한다)

  • 스프링에서 공급되는 @Autowired 키워드가 아닌 @Resource 키워드로 대체할 수 있음
    ➡️대체 키워드를 도입하는 이유 :
    일반적으로 스프링을 사용하지만 다른 프레임워크가 들어온다하더라도
    사용에 문제가 없도록 표준화하기 위하여 대체 키워드를 도입하였다.

  • 필드 의존성 주입과 설정자 의존성 주입에서 @Resource 어노테이션을 사용할 수 있는 것과 달리
    생성자 의존성 주입에서는 @Resource 어노테이션을 사용할 수 없다.

@Configuration vs @ComponentScan

➡️@Configuration :
Java 설정파일로, 설정파일을 읽어 스프링 컨테이너에 Bean을 등록
명시적으로 Bean을 생성하여 사용할때 적합함

➡️@ComponentScan :
지정된 패키지를 스캔하여 @Component관련 어노테이션이붙은 클래스들을 자동으로 Bean으로 등록
명시적으로 Bean을 생성하지 않아도됨

정리하자면 @Configuration 에서는 명시적으로 일부 Bean을 정의하고
나머지는 @ComponentScan으로 Bean을 자동 등록하는 방식을 사용

@Configuration
@ComponentScan(basePackages = "com.example.iocexam")
public class UserConfig {
    @Bean
    public UserDao userDao() {
        return new UserDaoImpl();
    }
}
  • ⭐따라서 두 방식은 명시적인 Bean 등록 (직접 생성) 과 자동 Bean 등록(스캔)의 차이
    프로젝트 요구 사항에 따라 적절히 선택

주요 JSR-250 어노테이션들

  • @Resource : 리소스나 서비스에 대한 참조를 주입받기 위해 사용 이름, 타입 등을 기반으로 의존성 주입
    (ex. 세션, 기타 환경 자원 등 주입에 활용)

  • @PostConstruct : 객체 생성과 의존성 주입이 완료된 후 초기화 목적으로 실행할 메소드에 사용
    이 메소드는 객체가 생성된 후 단 한번만 호출되고 초기화작업에서 수행

  • @PreDestroy : 컨테이너에 의해 빈이 제거되기 전 호출될 메소드에 사용
    ex. 리소스 해제, 정리작업 등
@PostConstruct
public void init(){
    // 해당 빈이 생성된 직후 이 메소드를 호출
    System.out.println("빈이 생성된 직후 호출됨.. PostConstruct 실행!");
}

@PreDestroy
public void destory(){
    System.out.println("빈이 소멸되기 전에 호출됨.. PreDestroy 실행!");
}

 


Annotaion

  • 발음에 따라 어노테이션, 애노테이션 둘 다 가능

IntelliJ - 어노테이션 생성 방법

@Retention(RetentionPolicy.RUNTIME)
public @interface Count100 {

}
  • @Retention : 어노테이션 패키지에서 제공하는 어노테이션 (어노테이션을 실행 시기 설정 가능)
  • @Retention(RetentionPolicy.RUNTIME)
    ➡️프로그램 실행 중에도 읽을 수 있음 (실행시에도 참조가능)
    (=어노테이션이 실행중에도 JVM에 의해 읽힐 수 있음)
    ➡️스프링에서 AOP(Aspect Oriented Programming)처럼 동적으로 메소드 실행 전/후 동작을 추가하는데 사용 가능

 

▶️실습 - 사용자 정의 어노테이션 사용

public class Hello {
    @Count100
    public void print(){
        System.out.println("Hello");
    }
}

 

public class HelloRun {
    public static void main(String[] args) {
        Hello hello = new Hello();
        hello.print();
    }
}
  • @Count100 어노테이션이 붙으면 약속된 일을 수행하도록함 

▶️실습 - 스프링 문법으로 메소드 가져오기

public class HelloRun {
    public static void main(String[] args) throws NoSuchMethodException {
        Hello hello = new Hello();

        Method method = hello.getClass().getDeclaredMethod("print");
        if(method.isAnnotationPresent(Count100.class)){
            // 어노테이션 중에 Count100이 붙은 메소드가 있는지 체크
            for(int i = 0; i < 100; i++){ // @Count100 어노테이션이 붙었으면 100번 출력
                hello.print();
            }
        }else{ // @Count100 어노테이션이 없으면 한번 출력
            hello.print(); // 
        }
    }
}
  • hello.getClass().getDeclaredMethod("print")
    ➡️getClass()와 getDeclaredMethod로 가져와서 Method타입에 담는다.

  • isAnnotationPresent()
    ➡️Count100 어노테이션이 존재하는지 확인


@Target 어노테이션

  • @Retention과 함께 어노테이션의 적용범위와 사용방법을 정의하는 “메타 어노테이션”
  • 어떤 “대상”에 어노테이션을 붙일 수 있는지 정의 (클래스, 메소드, 필드, 파라미터 등)

  • @Target(ElementType.METHOD) 처럼 사용
    ➡️
    ElementType을 METHOD로 정의했기때문에 메소드 위에서 어노테이션 정의가능
    ➡️메소드 위에 어노테이션 정의를 하게 되면 다른 클래스나 필드에서는 사용할 수 없도록 제한

▶️실습 - 사용자 정의 어노테이션 사용 2

  • getDeclaredMethods()
    ➡️getDeclaredMethod와 달리 배열을 반환하여 배열값을 받아올 수도 있다.

public static void main(String[] args) {
    Service service = new Service();
    
    Method[] declaredMethods = Service.class.getDeclaredMethods();
}
  • 만든 클래스의 정보들을 얻어올 수 있는 기능을 자바가 제공

  • Service.class.getDeclaredMethods()
    ➡️Service의 메소드들을 추상화한 배열을 얻어올 수 있는 것임


for(Method method : declaredMethods){
    if(method.isAnnotationPresent(PrintAnnotation.class)){
        PrintAnnotation printAnnotation = method.getAnnotation(PrintAnnotation.class);
        for(int i = 0; i < printAnnotation.number(); i++){
            System.out.println(printAnnotation.value());
        }
        System.out.println();
    }
}
  • isAnnotationPresent() : 사용자 정의 PrintAnnotation 어노테이션이 메소드에 붙어있는지 확인

  • method.getAnnotation(PrintAnnotation.class)
    ➡️printAnnotation 타입의 객체를 만드는데 getAnnotation()메소드를 활용하여
    @PrintAnnotation 이 붙은 메소드를 찾아 가져옴

public class Service {
    @PrintAnnotation
    public void methodA(){
        System.out.println("methodA 실행!");
    }

    @PrintAnnotation("BBB")
    public void methodB(){
        System.out.println("methodB 실행!");
    }

    @PrintAnnotation(number = 10)
    public void methodC(){
        System.out.println("methodC 실행!");
    }

    @PrintAnnotation(value = "#", number = 20)
    public void methodD(){
        System.out.println("methodD 실행!");
    }
}

  • 어노테이션의 속성에 따라 각기 다양한 동작을 할 수 있도록 구현 가능

🚀 회고를 통해 더 궁금했던 부분이었던

필드를 통한 의존성 주입, Optional 사용의 장단점, @Configuration과 @ComponentScan의 차이점 등을 공부할 수 있었다.

스프링이 익숙치 않아서인지 여전히 배울 것이 많은 것 같다.

아직까지는 완벽히 이해할 수 없어서 추가적으로 계속 공부를 해야할 것 같다.