🦁멋쟁이사자처럼 백엔드 부트캠프 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
- 발음에 따라 어노테이션, 애노테이션 둘 다 가능
@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의 차이점 등을 공부할 수 있었다.
스프링이 익숙치 않아서인지 여전히 배울 것이 많은 것 같다.
아직까지는 완벽히 이해할 수 없어서 추가적으로 계속 공부를 해야할 것 같다.
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_36일차_"스프링 MVC" (1) | 2025.01.22 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_35일차_"스프링 AOP" (1) | 2025.01.21 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_33일차_"스프링 DI/IoC" (1) | 2025.01.17 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_32일차_"스프링 프레임워크" (0) | 2025.01.16 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_31일차_"리액트 useEffect, Memo프로젝트" (1) | 2025.01.15 |