๋ชฉ์ฐจ
Spring์ ๋จ์ํ ํ๋ ์์ํฌ๊ฐ ์๋๋๋ค.
๊ฐ์ฒด ์งํฅ ์ค๊ณ, ์์กด์ฑ ์ฃผ์ , AOP, ์ค์ ์ ์ฐ์ฑ ๋ฑ ์ฌ๋ฌ ์ฒ ํ๊ณผ ๊ธฐ์ ์ด ๋ น์ ์๋ ์ข ํฉ ๊ฐ๋ฐ ํ๋ซํผ์ด์ฃ .
Spring Boot๋ก ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์ ๋ถ์ด๋ผ๋ฉด @Autowired, @Component ๊ฐ์ ์ด๋ ธํ ์ด์ ์ ์ต์ํ์ง๋ง
“๋๋์ฒด ๋ด๋ถ์์ ๋ฌด์จ ์ผ์ด ์ผ์ด๋๋ ๊ฑธ๊น?” ๋ผ๋ ์๋ฌธ์ด ํ ๋ฒ์ฏค์ ๋ค์์ ๊ฒ๋๋ค.
์ด๋ฒ ๊ธ์์๋ Spring์ ํต์ฌ ๊ฐ๋ ๋ค์ ์ค์ ์ฝ๋ ์์์ ์ค๋ฌด ํ์ฉ ํฌ์ธํธ ์ค์ฌ์ผ๋ก ์ ๋ฆฌํด๋ณด๊ฒ ์ต๋๋ค
๐ง ์ ์ด์ ์ญ์ (IoC)๊ณผ ์์กด์ฑ ์ฃผ์ (DI)
IoC(Inversion of Control) ๋?
IoC(Inversion of Control)๋ ๊ฐ์ฒด์ ์์ฑ๊ณผ ์์กด์ฑ ๊ด๋ฆฌ๋ฅผ ๊ฐ๋ฐ์๊ฐ ์๋ ์คํ๋ง ์ปจํ ์ด๋๊ฐ ๋ด๋นํ๋ ๊ตฌ์กฐ๋ฅผ ๋งํฉ๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ด IoC๋ฅผ ๊ตฌํํ๋ ๋ฐฉ์์ด ๋ฐ๋ก DI(Dependency Injection), ์ฆ ์์กด์ฑ ์ฃผ์ ์ ๋๋ค.
์ ํต์ ์ธ ๋ฐฉ์ (IoC ์ ์ฉ ์ )
public class UserService {
private UserRepository userRepository = new UserRepository(); // ์ง์ ์์ฑ
public User findUser(Long id) {
return userRepository.findById(id);
}
}
Spring IoC ์ ์ฉ ํ
@Service
public class UserService {
private final UserRepository userRepository;
// Spring์ด UserRepository๋ฅผ ์ฃผ์
ํด์ค
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findUser(Long id) {
return userRepository.findById(id);
}
}
๐ง ํต์ฌ ์ด๋ ธํ ์ด์
- @Component, @Service, @Repository: ์คํ๋ง์ด ์๋์ผ๋ก ๊ด๋ฆฌํ๋ ๋น ๋ฑ๋ก
- @Autowired: ์์กด์ฑ ์ฃผ์ ํ์ (์์ฑ์/ํ๋/์ธํฐ ๋ฐฉ์ ์ง์)
๐ก ํ์ฉ ํฌ์ธํธ
- ๊ฒฐํฉ๋๋ฅผ ๋ฎ์ถ๊ณ ํ ์คํธ ์ฉ์ด์ฑ ํฅ์
- ์คํ๋ง ์ปจํ ์ด๋๋ฅผ ํตํด ๋ชจ๋ ๊ฐ์ฒด(๋น)์ ๋ผ์ดํ์ฌ์ดํด์ ๊ด๋ฆฌ
DI์ 3๊ฐ์ง ๋ฐฉ์
1. ์์ฑ์ ์ฃผ์ (๊ถ์ฅ)
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
// ์์ฑ์๊ฐ ํ๋๋ฉด @Autowired ์๋ต ๊ฐ๋ฅ
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
}
2. ํ๋ ์ฃผ์ (ํ ์คํธํ๊ธฐ ์ด๋ ค์, ๋น์ถ์ฒ)
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // ๊ถ์ฅํ์ง ์์
}
3. ์ธํฐ ์ฃผ์ (์ ํ์ ์์กด์ฑ์ ์ฌ์ฉ)
@Service
public class UserService {
private UserRepository userRepository;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
์์ฑ์ ์ฃผ์ ์ final ํค์๋๋ฅผ ํ์ฉํด ๋ถ๋ณ์ฑ์ ๋ณด์ฅํ ์ ์๊ณ , Mock ์ฃผ์ ํ ์คํธ์๋ ์ ํฉํฉ๋๋ค.
๐งฌ Bean ์๋ช ์ฃผ๊ธฐ์ ์ค์ฝํ ์ดํดํ๊ธฐ
์คํ๋ง์ด ๊ด๋ฆฌํ๋ ๊ฐ์ฒด๋ฅผ Bean์ด๋ผ๊ณ ํฉ๋๋ค.
์ด Bean์ ์์ฑ๋ถํฐ ์๋ฉธ๊น์ง IoC ์ปจํ ์ด๋์ ์ํด ๊ด๋ฆฌ๋๋ฉฐ, ์ค์ฝํ(scope)์ ๋ฐ๋ผ ๊ทธ ์๋ช ๋ ๋ฌ๋ผ์ง๋๋ค.
์๋ช ์ฃผ๊ธฐ ํ์ด๋ณด๊ธฐ
- Bean ๊ฐ์ฒด ์์ฑ
- ์์กด์ฑ ์ฃผ์ (DI)
- ์ด๊ธฐํ (@PostConstruct)
- ์ ํ๋ฆฌ์ผ์ด์ ์คํ ์ค ์ฌ์ฉ
- ์ข ๋ฃ ์ ์๋ฉธ (@PreDestroy)
@Component
public class MyBean {
@PostConstruct
public void init() {
System.out.println("์ด๊ธฐํ ์๋ฃ");
}
@PreDestroy
public void destroy() {
System.out.println("์๋ฉธ ์ฒ๋ฆฌ");
}
}
Bean ์ค์ฝํ ์ข ๋ฅ
์ค์ฝํ | ์ค๋ช |
singleton | ์ ํ๋ฆฌ์ผ์ด์ ๋ด์์ ํ๋์ ์ธ์คํด์ค๋ง ์์ฑ (๊ธฐ๋ณธ๊ฐ) |
prototype | ์์ฒญํ ๋๋ง๋ค ์๋ก์ด ์ธ์คํด์ค ์์ฑ |
request | HTTP ์์ฒญ๋ง๋ค ํ๋ (์น ํ๊ฒฝ) |
session | ์ฌ์ฉ์ ์ธ์ ๋ง๋ค ํ๋ (์น ํ๊ฒฝ) |
๋๋ถ๋ถ์ ์๋น์ค ๋ก์ง์ singleton ์ค์ฝํ๋ก ์์ฑ๋๋ฉฐ, ํ์์ ๋ฐ๋ผ prototype ๋ฑ์ ์กฐํฉํฉ๋๋ค.
โจ AOP๋ก ์ค๋ณต ์ฝ๋ ์ค์ด๊ธฐ
AOP(Aspect-Oriented Programming, ๊ด์ ์งํฅ ํ๋ก๊ทธ๋๋ฐ)๋ ๊ณตํต ๊ด์ฌ์ฌ(Cross-cutting Concern)๋ฅผ ๋ถ๋ฆฌํ์ฌ
ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง์ ๋ ๊น๋ํ๊ฒ ์ ์งํ ์ ์๋๋ก ๋์ต๋๋ค.
AOP ์์ด ์์ฑํ ์ฝ๋์ ๋ฌธ์ ์
@Service
public class UserService {
public User createUser(String name, String email) {
// ๋ก๊น
์ฝ๋ - ์ค๋ณต!
log.info("์ฌ์ฉ์ ์์ฑ ์์: name={}, email={}", name, email);
// ๊ถํ ์ฒดํฌ ์ฝ๋ - ์ค๋ณต!
if (!SecurityUtils.hasPermission("USER_CREATE")) {
throw new AccessDeniedException("๊ถํ์ด ์์ต๋๋ค");
}
// ์คํ ์๊ฐ ์ธก์ - ์ค๋ณต!
long startTime = System.currentTimeMillis();
try {
// ์ค์ ๋น์ฆ๋์ค ๋ก์ง
User user = new User(name, email);
userRepository.save(user);
log.info("์ฌ์ฉ์ ์์ฑ ์๋ฃ: userId={}", user.getId());
return user;
} finally {
long endTime = System.currentTimeMillis();
log.info("์คํ ์๊ฐ: {}ms", endTime - startTime);
}
}
}
AOP๋ฅผ ์ ์ฉํ ๊น๋ํ ์ฝ๋
@Service
public class UserService {
@LogExecutionTime // ์คํ ์๊ฐ ์ธก์
@RequiredPermission("USER_CREATE") // ๊ถํ ์ฒดํฌ
@Transactional // ํธ๋์ญ์
๊ด๋ฆฌ
public User createUser(String name, String email) {
// ์์ํ ๋น์ฆ๋์ค ๋ก์ง๋ง!
User user = new User(name, email);
return userRepository.save(user);
}
}
๋น์ฆ๋์ค ๋ก์ง์ ๊ทธ๋๋ก ๋๊ณ , ๋ถ๊ฐ ๋ก์ง์ Aspect๋ก ๋ถ๋ฆฌํ ์ ์์ด ์ ์ง๋ณด์์ฑ์ด ๋ํญ ํฅ์๋ฉ๋๋ค.
์ค์ AOP ๊ตฌํ ์์
1. ์คํ ์๊ฐ ์ธก์ Aspect
@Aspect
@Component
@Slf4j
public class ExecutionTimeAspect {
@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed(); // ์ค์ ๋ฉ์๋ ์คํ
return result;
} finally {
long endTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
log.info("๋ฉ์๋ [{}] ์คํ ์๊ฐ: {}ms", methodName, endTime - startTime);
}
}
}
// ์ปค์คํ
์ด๋
ธํ
์ด์
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}
2. ์์ธ ์ฒ๋ฆฌ Aspect
@Aspect
@Component
@Slf4j
public class ExceptionHandlingAspect {
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))",
throwing = "exception")
public void handleException(JoinPoint joinPoint, Exception exception) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.error("๋ฉ์๋ [{}] ์คํ ์ค ์์ธ ๋ฐ์. ํ๋ผ๋ฏธํฐ: {}",
methodName, Arrays.toString(args), exception);
// ์ถ๊ฐ์ ์ธ ์์ธ ์ฒ๋ฆฌ ๋ก์ง (์๋ฆผ, ๋ฉํธ๋ฆญ ์์ง ๋ฑ)
notificationService.sendErrorAlert(methodName, exception);
}
}
โ๏ธ Spring Boot ์๋ ์ค์ ์ ๋น๋ฐ
Spring Boot์ ๊ฐ์ฅ ๊ฐ๋ ฅํ ํน์ง ์ค ํ๋๋ ์๋ ์ค์ (Auto Configuration)์ ๋๋ค.
๊ฐ๋ฐ์๊ฐ ๋ณ๋๋ก ์ค์ ํ์ง ์์๋, ํด๋์คํจ์ค์ ์กด์ฌํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ค์ ํ์ผ์ ๋ถ์ํด์ ํ์ํ ์ค์ ์ ์๋์ผ๋ก ๊ตฌ์ฑํด ์ค๋๋ค.
@SpringBootApplication์ด๋?
@SpringBootApplication
์ด ํ ์ค ์ด๋ ธํ ์ด์ ์ ์ฌ์ค ์ธ ๊ฐ์ง ํต์ฌ ์ด๋ ธํ ์ด์ ์ ์กฐํฉํ ๋ฉํ ์ด๋ ธํ ์ด์ (Meta-Annotation) ์ ๋๋ค.
- @Configuration: ๋น ๋ฑ๋ก ํด๋์ค
- @ComponentScan: ์ปดํฌ๋ํธ ์ค์บ
- @EnableAutoConfiguration: ์์กด์ฑ ๊ธฐ๋ฐ ์๋ ์ค์
์ฆ, @SpringBootApplication๋ง ๋ถ์ด๋ฉด ์คํ๋ง ๋ถํธ ์ ํ๋ฆฌ์ผ์ด์ ์ ์์ํ๋ ๋ฐ ํ์ํ ๊ฑฐ์ ๋ชจ๋ ์ค์ ์ด ์๋์ผ๋ก ์ค๋น๋ฉ๋๋ค.
๋์ ๋ฐฉ์
Spring Boot๋ META-INF/spring.factories ๋๋ ์ต์ ๋ฒ์ ์์๋
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ํ์ผ์ ์ฐธ๊ณ ํด์ ์๋ ์ค์ ํ๋ณด ํด๋์ค๋ฅผ ๋ก๋ฉํฉ๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ด ์ค์ ๋ค์ ์กฐ๊ฑด๋ถ ์ด๋ ธํ ์ด์ (@Conditional) ์ ๊ธฐ๋ฐ์ผ๋ก ์๋ํฉ๋๋ค.
@Bean
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
public DataSource dataSource() {
return new HikariDataSource();
}
์ค์ ์ ๋ชฐ๋ผ๋ ์ธ ์ ์์ง๋ง, ์๋ฆฌ๋ฅผ ์๋ฉด ๋๋ฒ๊น ๊ณผ ์ปค์คํฐ๋ง์ด์ง์ด ํจ์ฌ ์ฌ์์ง๋๋ค.
๐ง ์ธ๋ถ ์ค์ ๊ณผ ํ๋กํ์ผ๋ก ํ๊ฒฝ ๊ด๋ฆฌํ๊ธฐ
์ค๋ฌด์์๋ ๋ก์ปฌ, ๊ฐ๋ฐ, ์ด์ ํ๊ฒฝ๋ง๋ค ์ค์ ์ด ๋ฌ๋ผ์ผ ํฉ๋๋ค.
Spring Boot๋ ์ด๋ฅผ ์ํด ํ๋กํ์ผ(Profile) ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
ํ๋กํ์ผ๋ณ ์ค์ ํ์ผ ๊ตฌ์ฑ
src/main/resources/
โโโ application.yml # ๊ณตํต ์ค์
โโโ application-dev.yml # ๊ฐ๋ฐ ํ๊ฒฝ
โโโ application-test.yml # ํ
์คํธ ํ๊ฒฝ
โโโ application-prod.yml # ์ด์ ํ๊ฒฝ
# application.yml
spring:
profiles:
active: dev
# application-dev.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
# application-prod.yml
spring:
datasource:
url: jdbc:mysql://prod-db:3306/app
@ConfigurationProperties ์ฌ์ฉ
@ConfigurationProperties๋ฅผ ํ์ฉํ๋ฉด ์ธ๋ถ ์ค์ ์ ๊ฐ์ฒด๋ก ๋ฐ์ธ๋ฉํ ์ ์์ต๋๋ค.
@ConfigurationProperties(prefix = "app")
@Component
@Data
public class AppProperties {
private String name;
private String version;
private Security security = new Security();
private Mail mail = new Mail();
@Data
public static class Security {
private String jwtSecret;
private long jwtExpirationMs = 86400000; // 24์๊ฐ
private List<String> allowedOrigins = new ArrayList<>();
}
@Data
public static class Mail {
private String host;
private int port = 587;
private String username;
private String password;
private boolean enabled = true;
}
}
๐จ ์ค๋ฌด์์ ์์ฃผ ์ฐ์ด๋ ์คํ๋ง ๊ธฐ๋ฅ๋ค
์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ ์ฒ
// ์ด๋ฒคํธ ์ ์
public class UserRegisteredEvent {
private final String userId;
private final String email;
private final LocalDateTime registeredAt;
// ์์ฑ์, getter
}
// ์ด๋ฒคํธ ๋ฐํ
@Service
@RequiredArgsConstructor
public class UserService {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public User registerUser(String name, String email) {
User user = userRepository.save(new User(name, email));
// ์ด๋ฒคํธ ๋ฐํ
eventPublisher.publishEvent(
new UserRegisteredEvent(user.getId(), user.getEmail(), LocalDateTime.now())
);
return user;
}
}
// ์ด๋ฒคํธ ๋ฆฌ์ค๋
@Component
@Slf4j
public class UserEventListener {
@EventListener
@Async
public void handleUserRegistered(UserRegisteredEvent event) {
log.info("์ ์ฌ์ฉ์ ๋ฑ๋ก๋จ: {}", event.getUserId());
// ํ์ ์ด๋ฉ์ผ ๋ฐ์ก
emailService.sendWelcomeEmail(event.getEmail());
// ํต๊ณ ์
๋ฐ์ดํธ ๋ฑ
statisticsService.incrementUserCount();
}
}
์ค์ผ์ค๋ง
@Component
@EnableScheduling
@Slf4j
public class ScheduledTasks {
@Scheduled(fixedRate = 300000) // 5๋ถ๋ง๋ค
public void healthCheck() {
log.info("์์คํ
์ํ ์ฒดํฌ ์คํ");
systemHealthService.performHealthCheck();
}
@Scheduled(cron = "0 0 2 * * ?") // ๋งค์ผ ์๋ฒฝ 2์
public void dailyReportGeneration() {
log.info("์ผ์ผ ๋ฆฌํฌํธ ์์ฑ ์์");
reportService.generateDailyReport();
}
}
๐งก ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์ด๋ฒ ๊ธ์์๋ Spring์ ํต์ฌ ๊ฐ๋ ๋ค์ ์ค๋ฌด ๊ด์ ์์ ์ดํด๋ดค์ต๋๋ค.
๐ ํต์ฌ ์์ฝ
- IoC/DI: ๊ฐ์ฒด ๊ด๋ฆฌ๋ฅผ ์คํ๋ง์๊ฒ ๋งก๊ธฐ๊ณ , ๊ฒฐํฉ๋๋ฅผ ๋ฎ์ถฐ ํ ์คํธ์ ์ ์ง๋ณด์๊ฐ ์ฌ์ด ๊ตฌ์กฐ ์ค๊ณ
- Bean ์๋ช ์ฃผ๊ธฐ/์ค์ฝํ: ๊ฐ์ฒด ์๋ช ๊ณผ ์ฌ์ฉ ๋ฒ์๋ฅผ ๋ช ํํ ์ดํด
- AOP: ์ค๋ณต ๋ก์ง ์ ๊ฑฐ, ๋น์ฆ๋์ค ์ฝ๋์ ์์์ฑ ์ ์ง
- ํ๊ฒฝ ์ค์ /ํ๋กํ์ผ: ํ๊ฒฝ๋ณ ์ค์ ๋ถ๋ฆฌ๋ก ์ค๋ฌด ์ ์ฐ์ฑ ํ๋ณด
์ด ๊ฐ๋ ๋ค์ ์ ํํ ์ดํดํ๊ณ ๋๋ฉด, Spring Boot๋ฅผ ๋ ํจ๊ณผ์ ์ผ๋ก ํ์ฉํ ์ ์์ต๋๋ค.
ํนํ ์ค๋ฌด์์๋ AOP๋ฅผ ํตํ ๋ก๊น , ํ๋กํ์ผ์ ํตํ ํ๊ฒฝ ๋ถ๋ฆฌ, ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ ์ฒ ๋ฑ์ด ๋งค์ฐ ์ ์ฉํ๊ฒ ์ฌ์ฉ๋ผ์.
๋ค์ ๊ธ์์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋๊ณผ JPA์ ๋ํด ๋ค๋ค๋ณด๊ฒ ์ต๋๋ค.
๊ถ๊ธํ ์ ์ด๋ ๋ ์์ธํ ์๊ณ ์ถ์ ๊ฐ๋ ์ด ์๋ค๋ฉด ๋๊ธ๋ก ๋จ๊ฒจ์ฃผ์ธ์! ๐
'๐ฅ๏ธ Backend' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๐ง ๋ฐฑ์๋ ๊ฐ๋ฐ์๋ฅผ ์ํ JPA ํต์ฌ ์ ๋ฆฌ (0) | 2025.05.30 |
---|---|
๐ ๏ธ ๋ฐฑ์๋ ๊ฐ๋ฐ ํ๊ฒฝ ์ค์ A to Z (0) | 2025.05.28 |
๐ ์น์ ๊ธฐ๋ณธ ๊ฐ๋ ๊ณผ HTTP ์์ ์ ๋ณต (0) | 2025.05.27 |
๐ป ๋ฐฑ์๋๋ ๋ฌด์์ธ๊ฐ? (0) | 2025.05.20 |