지연 로딩은 연관된 엔티티 데이터를 처음에는 로드하지 않고, 실제로 필요할 때 데이터베이스에서 가져오는 전략입니다.
장점:
초기 로딩 시 불필요한 데이터를 가져오지 않아 성능 최적화.
연관된 엔티티가 필요하지 않을 경우 데이터베이스 접근을 줄일 수 있음.
작동 방식:
연관된 엔티티는 프록시(Proxy) 객체로 초기화.
프록시 객체는 원본 데이터를 대신하며, 실제 데이터가 필요할 때 데이터베이스에서 조회.
예제:
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Team team; // 지연 로딩
}
// 사용 시
Member member = entityManager.find(Member.class, 1L);
System.out.println(member.getName()); // 즉시 로드
System.out.println(member.getTeam().getName()); // 이 시점에 Team 데이터를 DB에서 조회
2. Fetch Join
Fetch Join은 JPQL에서 연관된 엔티티를 한 번에 가져오도록 명시적으로 지정하는 방법입니다.
장점:
여러 테이블을 조인하여 필요한 데이터를 한 번에 가져옴.
N+1 문제를 방지.
작동 방식:
JPQL에서 FETCH JOIN 키워드를 사용하면, 연관된 엔티티를 즉시 로딩하여 프록시 객체 대신 실제 엔티티 객체를 가져옵니다.
데이터베이스 조회 시점에 JOIN 쿼리가 실행되며, 필요한 모든 데이터를 로드.
예제:
String jpql = "SELECT m FROM Member m JOIN FETCH m.team";
List<Member> members = entityManager.createQuery(jpql, Member.class).getResultList();
for (Member member : members) {
System.out.println(member.getName()); // 즉시 로드
System.out.println(member.getTeam().getName()); // Team도 즉시 로드 (프록시 X)
}
Fetch Join이 우선권을 가지는 이유
1. Fetch Join은 JPQL에서 명시적으로 즉시 로딩을 요구
지연 로딩(LAZY) 설정이 되어 있더라도, FETCH JOIN이 사용되면 JPA가 지연 로딩 대신 즉시 로딩을 수행합니다.
이는 JPQL에서 개발자가 명시적으로 데이터 로딩 방식을 지정했기 때문입니다.
2. 프록시 대신 실제 엔티티 반환
FETCH JOIN을 사용하면 연관된 엔티티도 함께 로드됩니다.
따라서 연관된 엔티티는 프록시 객체가 아닌 실제 엔티티 객체로 초기화됩니다.
예제 비교:
지연 로딩 설정 (기본):
Member member = entityManager.find(Member.class, 1L); // Team은 로드되지 않음
System.out.println(member.getTeam().getName()); // 이 시점에 Team을 DB에서 조회
Fetch Join 사용:
String jpql = "SELECT m FROM Member m JOIN FETCH m.team";
Member member = entityManager.createQuery(jpql, Member.class).getSingleResult();
// 이미 Team이 로드됨
System.out.println(member.getTeam().getName()); // 추가 DB 조회 없음
Fetch Join 후 모든 엔티티가 영속성 컨텍스트로 관리
1. 영속성 컨텍스트의 기본 동작
JPA는 엔티티를 조회할 때, 해당 엔티티를 영속성 컨텍스트에 저장하고 관리합니다.
Fetch Join으로 조회한 연관된 엔티티도 영속성 컨텍스트에서 관리됩니다.
2. Fetch Join의 효과
Fetch Join을 사용하면 연관된 엔티티도 데이터베이스에서 즉시 로드되며, 영속성 컨텍스트에 저장됩니다.
결과적으로 Fetch Join으로 조회된 엔티티는 모두 영속 상태로 관리됩니다.
예제:
String jpql = "SELECT m FROM Member m JOIN FETCH m.team";
List<Member> members = entityManager.createQuery(jpql, Member.class).getResultList();
for (Member member : members) {
System.out.println(entityManager.contains(member)); // true (영속 상태)
System.out.println(entityManager.contains(member.getTeam())); // true (영속 상태)
}
Fetch Join이 Proxy 객체 대신 실제 객체를 반환하는 이유
1. 명시적 로딩
Fetch Join은 연관된 엔티티를 즉시 로딩하기 때문에, 데이터베이스 조회 시점에 실제 데이터로 초기화된 엔티티를 반환합니다.
2. N+1 문제 방지
Fetch Join은 한 번의 쿼리로 여러 엔티티를 로드하므로, **지연 로딩에서 발생할 수 있는 추가 쿼리(N+1 문제)**를 방지합니다.
3. 프록시 대신 실제 엔티티 반환
Fetch Join은 데이터베이스에서 가져온 데이터를 기반으로 연관된 엔티티를 실제 객체로 초기화하므로, 프록시 객체가 필요 없습니다.
Fetch Join의 쿼리 예제
JPQL:
String jpql = "SELECT m FROM Member m JOIN FETCH m.team";
List<Member> members = entityManager.createQuery(jpql, Member.class).getResultList();
실행된 SQL:
SELECT m.*, t.*
FROM Member m
JOIN Team t ON m.team_id = t.id;
결과: Member와 Team 엔티티가 함께 로드되며, 추가 쿼리가 발생하지 않음.
Fetch Join의 장점
성능 최적화:
한 번의 쿼리로 연관된 엔티티를 함께 로드하므로 데이터베이스 접근 횟수를 줄임.
N+1 문제 해결:
Fetch Join을 사용하면 지연 로딩으로 인한 추가 쿼리(N+1 문제)가 발생하지 않음.
일관된 데이터 상태:
연관된 엔티티가 즉시 로드되어 프록시 객체 대신 실제 객체로 사용 가능.
주의사항
데이터 양 증가:
Fetch Join으로 많은 연관 데이터를 한 번에 로드하면, 불필요한 데이터까지 조회하여 메모리 사용량이 증가할 수 있음.
Fetch Join 제한:
JPQL에서 하나의 Fetch Join만 허용되며, 복잡한 다중 Fetch Join은 지원하지 않음.
예: @OneToMany와 @ManyToOne을 동시에 Fetch Join하면 문제가 발생할 수 있음.
결론
Fetch Join이 지연 로딩보다 우선:
JPQL에서 명시적으로 Fetch Join을 사용하면 지연 로딩 설정을 무시하고 즉시 로딩이 수행됩니다.
영속성 컨텍스트 관리:
Fetch Join으로 조회된 모든 엔티티는 프록시 객체가 아닌 실제 객체로 초기화되며, 영속성 컨텍스트에서 관리됩니다.
성능 최적화:
Fetch Join은 한 번의 쿼리로 연관된 데이터를 가져와 N+1 문제를 해결하고 성능을 최적화하는 강력한 도구입니다.
Collection fetch join
📌 @OneToMany 의 기본 FetchType은 LAZY 이다.
1:N fetch join(Collection)
String query = "select c from Company c join fetch c.tutorList";
List<Company> companyList = em.createQuery(query, Company.class).getResultList();
for (Company company : companyList) {
System.out.println("company.getName() = " + company.getName());
System.out.println("company.getTutorList().size() = " + company.getTutorList().size());
}
데이터 중복이 발생하지 않는다.
SQL Query의 조회 결과는 데이터가 중복된다.
Hibernate 6.0 이상 부터는 DISTINCT가 자동으로 적용된다.
JPQL의 DISTINCT
Database의 DISTINCT 는 완전히 데이터가 같아야 중복이 제거된다.
JPQL의 DISTINCT 는 같은 PK값을 가진 Entity를 제거한다.
주의점
Collection 에 fetch join을 사용하면 페이징을 메모리에서 수행한다.
코드 예시
String query = "select c from Company c join fetch c.tutorList";
List<Company> companyList = em.createQuery(query, Company.class)
.setFirstResult(0)
.setMaxResults(1)
.getResultList();
전체를 조회하는 SQL이 실행된다.
setFirstResult(), setMaxResult() 가 SQL에 반영되지 않는다.
모든 데이터를 조회하여 메모리에서 페이징을 처리한다.
필요없는 데이터까지 전체 로드한 후 필터링한다.
@BatchSize
📌 JPA에서 N+1 문제를 해결하기 위해 사용되는 설정으로 지연 로딩(Lazy Loading) 시 한 번에 로드할 엔티티의 개수를 조정하여 여러 개의 엔티티를 효율적으로 조회할 수 있다.
String query = "select c from Company c";
List<Company> companyList = em.createQuery(query, Company.class)
.setFirstResult(0)
.setMaxResults(2)
.getResultList();
System.out.println("companyList.size() = " + companyList.size());
for (Company company : companyList) {
System.out.println("company.getName() = " + company.getName());
for (Tutor tutor : company.getTutorList()) {
System.out.println("tutor.getName(): " + tutor.getName());
}
}
Company 전체 조회
조회 결과 2개(sparta, etc)에 각각 지연 로딩
@BatchSize
@Entity
@Table(name = "company")
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@BatchSize(size = 100)
@OneToMany(mappedBy = "company")
private List<Tutor> tutorList = new ArrayList<>();
public Company() {
}
public Company(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public List<Tutor> getTutorList() {
return tutorList;
}
}
한 번의 IN Query에 식별자(PK)를 조회된 개수만큼 넣어준다.
설정 파일의 hibernate.jdbc.batch_size 를 통해 Global 적용이 가능하다.
xml, yml, properties 모두 가능
정리
JPQL의 한계
동적 쿼리를 사용하기 어렵다.
SQL을 문자열로 작성하여 사용하기 까다롭다.
SQL의 모든 기능을 사용할 수 없다(Native Query 사용).
fetch join 정리
SQL의 JOIN과 비슷하지만 연관된 엔티티나 컬렉션을 한 번의 쿼리로 함께 로드하는 기능.
@RestController
@RequiredArgsConstructor
public class ExceptionHandlerController {
private final ExceptionHandlerService exceptionHandlerService;
@RequestMapping("/v1/exception")
public void illegalArgumentException() {
throw new IllegalArgumentException("IllegalArgumentException 발생");
}
@RequestMapping("/v2/exception")
public void nullPointerException() {
throw new NullPointerException("NPE 발생");
}
@RequestMapping("/v3/exception")
public void serviceLayerException() {
exceptionHandlerService.throwNewIllegalArgumentException();
}
// IllegalArgumentException을 처리하는 메서드
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, String>> handleIllegalArgumentException(IllegalArgumentException ex) {
Map<String, String> response = new HashMap<>();
response.put("error", ex.getMessage());
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(response);
}
}
@RestController
public class NonExceptionHandlerController {
@RequestMapping("/v4/exception")
public void illegalArgumentException() {
throw new IllegalArgumentException("IllegalArgumentException 발생");
}
}
@Service
public class ExceptionHandlerService {
public void throwNewIllegalArgumentException() {
throw new IllegalArgumentException("ServiceLayer Exception");
}
}
문제점
Controller 코드에 Exception 처리를 위한 책임이 추가된다.(단일 책임 원칙 위반)
단일 컨트롤러 내의 예외만 처리가 가능하다.(컨트롤러 예외처리 중복코드)
코드 재사용, 유지보수성 부족
CustomException 사용자 정의 Exception을 만들어서 처리할 수 있다.
@ControllerAdvice
📌 @ControllerAdvice와 동일한 기능을 제공하지만 @RestController를 포함하고 있어 반환 값이 자동으로 JSON 형태로 변환되며 REST API에서 발생하는 예외를 처리할 때 사용한다.
Application 전역에서 발생한 예외를 처리한다.
@RestControllerAdvice
REST API의 예외를 처리한다.
반환 값이 JSON이다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, String>> handleIllegalArgumentException(IllegalArgumentException ex) {
Map<String, String> response = new HashMap<>();
response.put("error", ex.getMessage());
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(response);
}
}
@Slf4j
public class PriceFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
log.info("text = {}, locale={}", text, locale);
// 변환 로직
// NumberFormat이 제공하는 기능
NumberFormat format = NumberFormat.getInstance(locale);
// "10,000" -> 10000L
return format.parse(text);
}
@Override
public String print(Number object, Locale locale) {
log.info("object = {}, locale = {}", object, locale);
// 10000L -> "10,000"
return NumberFormat.getInstance(locale).format(object);
}
}
Number
Integer, Long, Double 등의 부모 클래스
class PriceFormatterTest {
PriceFormatter formatter = new PriceFormatter();
@Test
void parse() throws ParseException {
// given, when
Number result = formatter.parse("1,000", Locale.KOREA);
// then
// parse 결과는 Long
Assertions.assertThat(result).isEqualTo(1000L);
}
@Test
void print() {
// given, when
String result = formatter.print(1000, Locale.KOREA);
// then
Assertions.assertThat(result).isEqualTo("1,000");
}
}
Spring Formatter
📌 Spring의 Formatter는 문자열 데이터를 특정 객체로 변환하거나, 객체를 특정 문자열 형식으로 변환(포맷팅)하는 데 사용되는 인터페이스입니다. 데이터 포맷팅과 역변환을 쉽게 처리할 수 있도록 Spring에서 제공됩니다
FormattingConversionService
📌 ConversionService와 Formatter를 결합한 구현체로 타입 변환과 포맷팅이 필요한 모든 작업을 한 곳에서 수행할 수 있도록 설계되어 있어서 다양한 타입의 변환과 포맷팅을 쉽게 적용할 수 있다.
public class FormattingConversionServiceTest {
@Test
void formattingConversionService() {
// given
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
// Converter 등록
conversionService.addConverter(new StringToPersonConverter());
conversionService.addConverter(new PersonToStringConverter());
// Formatter 등록
conversionService.addFormatter(new PriceFormatter());
// when
String result = conversionService.convert(10000, String.class);
// then
Assertions.assertThat(result).isEqualTo("10,000");
}
}
ConversionService가 제공하는 convert()를 사용하면 된다.
SpringBoot의 기능
📌 SpringBoot는 기본적으로 WebConversionService를 사용한다.
DefaultFormattingConversionService 상속
Spring이 제공하는 Formatter
📌 Spring은 어노테이션 기반으로 원하는 형식의 Formatter를 사용할 수 있도록 기능을 제공한다.
📌 Spring에서 객체의 타입을 서로 변환하는 데 사용되는 인터페이스로 Spring의 데이터 바인딩 과정에서 문자열을 특정 객체로 변환하거나 하나의 객체 타입을 다른 타입으로 변환할 때 사용한다.
문자를 숫자로, 숫자를 문자로 변환하는 등 Web Application을 만들다보면 Type을 변환해야 하는 경우가 많이 발생한다.
결론
요청 파라미터로 전달하는 10 값은 실제로는 문자열(String) 10이다.
@RequestParam을 사용하면 문자 10을 Integer 타입의 숫자 10으로 변환된다.
@ModelAttribute, @PathVariable 에서도 타입 변환을 확인할 수 있다.
Spring 내부에서 누군가가 타입을 자동으로 변환한다.
Converter Interface
Spring이 제공하는 인터페이스
implements하여 Converter로 등록하면 된다.
Converter는 모든 타입(T)에 적용할 수 있다.
개발자가 새로운 Type을 만들어서 사용할 수 있도록 만든다.
변환하고자 하는 타입에 맞춰서 Type Converter를 구현하고 등록하면 된다.
Converter
📌 데이터 타입 간 변환을 처리하는 인터페이스 , 주로 웹 요청 파라미터를 Java 객체로 변환하거나 그 반대로 변환할 때 사용되며 커스텀 변환 로직을 정의할 수 있다.
주의점
org.springframework.core.convert.converter
Spring의 Converter와 같은 이름을 가진 Interface가 많으니 주의해야 한다.
코드 예시
String → Integer
Converter<S, T> 에서 S는 변환할 Source T는 변환할 Type으로 설정하면 된다.
@Slf4j
public class StringToIntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
log.info("source = {}", source);
// 검증
return Integer.valueOf(source);
}
}
파라미터로 들어온 source가 Interger로 변환된다.
Integer → String
@Slf4j
public class IntegerToStringConverter implements Converter<Integer, String> {
@Override
public String convert(Integer source) {
log.info("source = {}", source);
return String.valueOf(source);
}
}
파라미터로 들어온 source가 String으로 변환된다.
String → Person
@Getter
public class Person {
// 이름
private String name;
// 나이
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
요청 예시
localhost:8080/type-converter?person=wonuk:1200
public class StringToPersonConverter implements Converter<String, Person> {
// source = "wonuk:1200"
@Override
public Person convert(String source) {
// ':' 를 구분자로 나누어 배열로 만든다.
String[] parts = source.split(":");
// 첫번째 배열은 이름이다. -> wonuk
String name = parts[0];
// 두번째 배열은 개월수이다. -> 1200
int months = Integer.parseInt(parts[1]);
// 개월수 나누기 12로 나이를 구하는 로직 (12개월 단위만 고려)
int age = months / 12;
return new Person(name, age);
}
}
public class PersonToStringConverter implements Converter<Person, String> {
@Override
public String convert(Person source) {
// 이름
String name = source.getName();
// 개월수
int months = source.getAge * 12;
// "wonuk:1200"
return name + ":" + months;
}
}
TypeConverter 사용
구현은 단순하게 직접 메서드를 구현하여 모듈화 하면된다.
TypeConverter 를 생성하여 직접 사용하면 컨트롤러에서 변환하는 방식과 큰 차이가 없다.
PersonToStringConverter converter = new PersonToStringConverter();
String source = "wonuk:1200";
converter.convert(source);
Converter를 편리하게 등록하고 사용할 수 있도록 만들어주는 기능이 필요하다.
Spring은 String, Integer, Enum등 자주 사용되는 타입에 대한 컨버터를 제공하고 사용할 수 있도록 등록되어 있다.
Spring의 다양한 Converter
📌 Spring에서 제공하는 다양한 Converter 인터페이스가 존재하며 이들은 Spring의 데이터 바인딩, 요청/응답 처리, 속성 값 주입 등에 사용되고 ConversionService를 통해 등록 및 관리된다.
Converter
기본적인 변환을 담당하는 인터페이스
단일 타입에서 단일 타입으로 변환할 때 사용한다.
Converter<Source, Type>
ConverterFactory
클래스 계층 구조가 복잡한 경우 사용
기본 타입과 다양한 서브 타입 간의 변환을 지원한다.
GenericConverter
다양한 타입 간의 유연한 변환을 지원한다.
복잡한 타입 변환 로직을 구현할 때 유리하다.
ConditionalGenericConverter
GenericConverter 의 확장형으로 특정 조건에서만 타입 변환을 수행한다.
추가적으로 matches() 를 통해 변환 가능 여부를 판단할 수 있다.
ConversionService
📌 Spring은 Converter를 모아서 편리하게 관리하고 사용할 수 있게 해주는 기능을 제공한다. 이것이 Conversion Service 이다.
ConversionService 인터페이스
canConvert()
Convert 가능 여부를 확인하는 기능
convert()
실제 변환하는 기능
DefaultConversionService
📌 Spring의 표준 ConversionService로 기본 제공 Converter와 확장 가능성을 통해 다양한 타입 변환을 유연하게 처리할 수 있도록 지원한다.
DefaultConversionService
ConversionService를 구현한 구현체
ConvertRegistry에 다양한 Converter를 등록한다.
ConverterRegistry
Converter를 등록하고 관리하는 기능을 제공한다.
import static org.assertj.core.api.Assertions.*;
public class ConversionServiceTest {
@Test
void defaultConversionService() {
// given
DefaultConversionService dcs = new DefaultConversionService();
dcs.addConverter(new StringToPersonConverter());
Person wonuk = new Person("wonuk", 100);
// when
Person stringToPerson = dcs.convert("wonuk:1200", Person.class);
// then
assertThat(stringToPerson.getName()).isEqualTo(wonuk.getName());
assertThat(stringToPerson.getAge()).isEqualTo(wonuk.getAge());
}
}
컨버터를 사용할 때는 종류를 몰라도된다.
컨버터는 ConversionService 내부에서 숨겨진채 제공된다.
반환 타입, 파라미터 타입, 제네릭 등으로 ConversionService가 컨버터를 찾는다.
즉, 클라이언트는 ConversionService 인터페이스만 의존하면 된다.
컨버터 등록과 사용의 분리
ISP(인터페이스 분리 원칙, Interface Segregation Principal)
📌 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 분리하는 원칙
DefaultConversionService
ConversionRegistry : 컨버터 등록
ConversionService ****: ****컨버터 사용
인터페이스를 분리하면 컨버터를 사용하는 클라이언트는 필요한 메서드만 알면된다.
ConversionRegistry 가 변경되어도 ConversionService와 연관이 없다.
Spring은 내부적으로 위와같이 등록, 사용이 분리된 인터페이스들이 아주 많다.
Spring은 내부적으로 ConversionService를 사용해 타입을 변환한다. 대표적으로 @RequestParam , @PathVariable, @ModelAttribute 등이 해당 기능을 사용한다.
Converter 요약
Spring의 **Converter**는 데이터 타입 간 변환을 처리하는 인터페이스입니다. 주로 Spring의 데이터 바인딩 또는 사용자 정의 변환 작업에 사용됩니다. 간단한 입력 값 변환에서부터 복잡한 객체 변환까지 유연하게 지원합니다.
Converter의 주요 특징
데이터 타입 변환:
소스 타입(Source Type)에서 대상 타입(Target Type)으로 변환.
예: String → Integer, String → LocalDate.
간결한 인터페이스:
구현이 간단하며 특정 변환 작업에 집중.
범용 사용 가능:
Spring의 데이터 바인딩, 요청 파라미터 변환, 커스텀 변환 로직 등에 사용.
확장 가능:
Spring이 기본으로 제공하는 변환기 외에도 사용자 정의 변환기를 구현할 수 있음.
Converter 인터페이스
public interface Converter<S, T> {
T convert(S source);
}
S: 소스 데이터 타입 (변환 전 데이터 타입).
T: 대상 데이터 타입 (변환 후 데이터 타입).
Converter의 사용 사례
1. 문자열을 날짜로 변환 (String → LocalDate)
public class StringToLocalDateConverter implements Converter<String, LocalDate> {
@Override
public LocalDate convert(String source) {
// 문자열을 LocalDate로 변환
return LocalDate.parse(source, DateTimeFormatter.ISO_DATE);
}
}
Spring에서 Converter 등록
Spring에서는 변환기를 전역적으로 등록하거나, 특정 컨텍스트에서 사용할 수 있습니다.
1. 전역 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// 커스텀 Converter 등록
registry.addConverter(new StringToLocalDateConverter());
}
}
📌 클라이언트와 서버 간의 HTTP 요청과 응답을 처리할 때 데이터 형식 변환을 담당 한다. 클라이언트가 보낸 데이터를 서버가 이해할 수 있는 형태로 변환하거나, 서버가 응답으로 보내는 데이터를 클라이언트가 이해할 수 있는 형태로 변환할 때 사용됩니다.[ View를 응답하는 것이 아닌, Rest API(HTTP API)로 JSON, TEXT, XML 등의 데이터를 응답 Message Body에 직접 입력하는 경우 HttpMessageConverter를 사용한다. ]
1. SSR → @Controller + View Template → 서버 측에서 화면을 동적으로 그린다.
2. CSR → @RestController + Data → 클라이언트 측에서 화면을 동적으로 그린다.
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType)는 Spring Framework의 HttpMessageConverter 인터페이스에서 정의된 메서드 중 하나로, 특정 타입의 객체를 지정된 미디어 타입(MediaType)으로 변환할 수 있는지 확인합니다. 이 메서드는 HTTP 응답을 생성할 때 사용됩니다.
이 매개변수를 통해 현재 HttpMessageConverter가 처리 가능한 객체인지 확인합니다.
@Nullable MediaType mediaType:
변환 대상의 **미디어 타입(MediaType)**입니다. 예: application/json, application/xml, text/plain 등.
@Nullable이므로 null일 수도 있습니다.
null인 경우 미디어 타입과 관계없이 클래스 타입만 확인합니다.
반환값
true: 이 HttpMessageConverter가 지정된 클래스와 미디어 타입에 대해 쓰기 작업(HTTP 응답 데이터 변환)을 처리할 수 있습니다.
false: 처리할 수 없으면 false를 반환합니다.
메서드 동작
이 메서드는 Spring이 적절한 **HttpMessageConverter**를 선택할 때 사용됩니다. 요청 데이터의 Java 객체 타입과 응답 데이터의 미디어 타입을 비교하여 적합한 컨버터를 결정합니다.
Spring은 등록된 여러 HttpMessageConverter를 순차적으로 탐색합니다.
각 컨버터의 canWrite 메서드를 호출하여, 특정 객체와 미디어 타입을 처리할 수 있는지 확인합니다.
적합한 컨버터를 찾으면 해당 컨버터가 쓰기 작업을 수행합니다.
사용 예시
JSON 변환 컨버터 예
MappingJackson2HttpMessageConverter는 JSON 데이터를 처리하는 HttpMessageConverter 구현체입니다. 이를 기준으로 canWrite 메서드를 구현하면 다음과 같이 동작합니다:
@Override
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
// JSON 컨버터는 기본적으로 모든 객체 타입을 지원
if (mediaType == null || mediaType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
return true; // JSON 형식으로 변환 가능
}
return false; // JSON 외 미디어 타입은 변환 불가
}
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)는 Spring의 HttpMessageConverter 인터페이스에서 정의된 메서드로, 데이터를 HTTP 응답 메시지에 쓰는 역할을 합니다. 이 메서드는 REST API에서 서버가 클라이언트로 데이터를 반환할 때 사용됩니다.
**ReturnValueHandler**는 컨트롤러 메서드가 반환한 데이터를 클라이언트에게 적절히 변환하여 응답으로 전달합니다.
대표적인 ReturnValueHandler
Handler 이름 설명
RequestResponseBodyMethodProcessor
@ResponseBody와 HttpEntity를 처리. Java 객체를 JSON/XML 등으로 변환.
ModelAndViewMethodReturnValueHandler
ModelAndView 객체를 처리하여 뷰를 렌더링.
ViewMethodReturnValueHandler
View 객체를 처리하여 뷰를 렌더링.
HttpEntityMethodProcessor
HttpEntity<T> 및 ResponseEntity<T>를 처리. 헤더와 본문을 설정하여 응답.
ModelMethodProcessor
모델 객체를 처리하여 View에서 사용할 데이터로 추가.
DeferredResultMethodReturnValueHandler
비동기 작업 결과를 처리(DeferredResult, CompletableFuture 등).
CallableMethodReturnValueHandler
Callable 객체를 처리하여 비동기 응답 생성.
StringMethodReturnValueHandler
단순 문자열(String)을 처리하여 응답 본문에 쓰거나, 뷰 이름으로 해석.
ReturnValueHandler의 예제
@ResponseBody와 ResponseEntity 사용
@RestController
public class ExampleController {
@GetMapping("/json")
public MyDto getJson() {
return new MyDto("data", 123); // RequestResponseBodyMethodProcessor 처리
}
@GetMapping("/response")
public ResponseEntity<String> getResponse() {
return ResponseEntity.ok("OK"); // HttpEntityMethodProcessor 처리
}
}
RequestResponseBodyMethodProcessor:
MyDto 객체를 JSON 형식으로 변환하여 응답 본문에 작성.
HttpEntityMethodProcessor:
ResponseEntity의 상태 코드, 헤더, 본문을 설정하여 응답.
전체 흐름 정리
Spring MVC의 요청 처리 과정에서 **ArgumentResolver**와 **ReturnValueHandler**는 다음처럼 동작합니다:
HttpMessageConverter는 Spring MVC에서 클라이언트와 서버 간 데이터를 변환하는 데 사용되는 컴포넌트입니다. HTTP 요청과 응답의 본문을 Java 객체와 JSON, XML, TEXT 등으로 상호 변환합니다.
HttpMessageConverter의 역할
요청 처리:
클라이언트가 JSON, XML 등 형식으로 요청 본문을 보내면 이를 Java 객체로 변환.
응답 생성:
컨트롤러가 반환한 Java 객체를 JSON, XML, TEXT 등으로 변환하여 클라이언트로 전송.
일상적인 비유
비유: 번역가(Translator)
한 사람이 영어로 말을 하고, 다른 사람이 한국어로 이해하려면 번역가가 필요합니다.
여기서 영어는 JSON, XML 등의 데이터 형식이고, 한국어는 Java 객체입니다.
번역가(HttpMessageConverter)가 데이터를 양쪽에서 서로 이해할 수 있도록 변환합니다.
HttpMessageConverter의 동작 흐름
클라이언트 요청:
클라이언트가 JSON 요청을 보냅니다.
예: {"name": "John", "age": 30}
요청 데이터 변환:
HttpMessageConverter가 JSON 데이터를 Java 객체로 변환.
컨트롤러 처리:
컨트롤러 메서드에서 비즈니스 로직 처리.
응답 데이터 변환:
컨트롤러 메서드가 반환한 Java 객체를 JSON 형식으로 변환하여 클라이언트에 응답.
대표적인 HttpMessageConverter 구현체
Converter 이름 역할
MappingJackson2HttpMessageConverter
JSON 데이터를 Java 객체로 변환 또는 반대로 변환.
MappingJackson2XmlHttpMessageConverter
XML 데이터를 Java 객체로 변환 또는 반대로 변환.
StringHttpMessageConverter
문자열 데이터를 처리.
FormHttpMessageConverter
application/x-www-form-urlencoded 폼 데이터를 처리.
ByteArrayHttpMessageConverter
바이너리 데이터를 처리 (예: 파일 다운로드).
HttpMessageConverter 사용 예제
요청 본문(JSON) → Java 객체 (@RequestBody)
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public String createUser(@RequestBody UserDto userDto) {
return "User created: " + userDto.getName();
}
}
클라이언트 요청:
POST /api/users Content-Type: application/json { "name": "John", "age": 30 }
HttpMessageConverter 동작:
MappingJackson2HttpMessageConverter가 JSON 데이터를 UserDto 객체로 변환.
컨트롤러는 변환된 UserDto 객체를 사용.
컨트롤러 결과:
User created: John
Java 객체 → JSON 응답 (@ResponseBody)
@RestController
public class ExampleController {
@GetMapping("/user/{id}")
public UserDto getUser(@PathVariable Long id) {
return new UserDto(id, "John", 30); // Java 객체 반환
}
}
IoC (Inversion of Control)와 DI (Dependency Injection)의 개념과 비유
1. IoC (Inversion of Control, 제어의 역전)
개념
IoC는 객체의 생성과 관리를 개발자가 아닌 Spring 컨테이너가 담당하는 설계 원칙입니다.
애플리케이션의 흐름 제어를 개발자가 아닌 **프레임워크(Spring)**가 제어하게 합니다.
비유
비유: 요리사가 아닌 **주방장(매니저)**가 모든 요리사를 관리하는 상황.
기존 방식: 요리사가 직접 재료를 구입하고 요리를 만들며 모든 과정을 제어.
IoC 방식: 주방장이 재료를 준비해주고 요리사에게 요리를 맡기는 방식. 요리사는 재료 준비에 신경 쓸 필요 없이 요리에만 집중.
2. DI (Dependency Injection, 의존성 주입)
개념
DI는 객체가 필요로 하는 의존성을 객체 내부에서 직접 생성하지 않고, 외부에서 주입해주는 방식입니다.
IoC의 구현 방법 중 하나로, 객체 간의 결합도를 낮추고 유연성을 높입니다.
비유
비유: 요리사가 재료를 직접 준비하지 않고 **매니저(주방장)**가 필요한 재료를 가져다 주는 상황.
기존 방식: 요리사가 요리마다 필요한 재료를 직접 슈퍼마켓에서 사와야 함.
DI 방식: 매니저가 요리사가 요청한 재료를 준비해 전달. 요리사는 요리(비즈니스 로직)에만 집중.
3. IoC와 DI를 연결하는 비유
비유 상황
레스토랑 시스템:
IoC:
주방장이 전체 흐름을 제어(객체 생성, 관리).
요리사(객체)는 주방장(Spring 컨테이너)에 의해 관리됨.
DI:
주방장이 요리사가 필요한 재료(의존성)를 제공(주입).
4. 예제 코드
IoC와 DI 적용 전 (전통적인 방식)
public class Chef {
private Ingredient ingredient;
public Chef() {
this.ingredient = new Ingredient(); // 요리사가 직접 재료를 생성
}
public void cook() {
System.out.println("Cooking with " + ingredient.getName());
}
}
public class Ingredient {
public String getName() {
return "Tomatoes";
}
}
문제점:
Chef 클래스가 Ingredient 클래스를 직접 생성(강한 결합).
Ingredient를 변경하려면 Chef도 수정해야 함.
IoC와 DI 적용 후
public class Chef {
private Ingredient ingredient;
// 의존성 주입 (DI)
public Chef(Ingredient ingredient) {
this.ingredient = ingredient;
}
public void cook() {
System.out.println("Cooking with " + ingredient.getName());
}
}
public class Ingredient {
public String getName() {
return "Tomatoes";
}
}
// Spring Configuration
@Configuration
public class AppConfig {
@Bean
public Ingredient ingredient() {
return new Ingredient(); // 의존성 생성
}
@Bean
public Chef chef(Ingredient ingredient) {
return new Chef(ingredient); // 의존성 주입
}
}
장점:
Chef 클래스는 Ingredient에 대해 아무것도 몰라도 됨.
Ingredient를 변경해도 Chef 클래스는 수정할 필요가 없음(유연성 증가).
5. IoC와 DI 요약
개념
설명
비유
IoC
객체의 생성 및 생명주기 관리를 개발자가 아닌 Spring 컨테이너가 담당.
주방장이 전체 요리 과정을 관리하고 제어.
DI
객체가 필요한 의존성을 외부에서 주입받아 사용.
주방장이 요리사가 필요한 재료를 전달. 요리사는 요리에만 집중.
IoC + DI 결합
IoC로 객체를 관리하고, DI로 객체 간 의존성을 주입하여 객체 간 결합도를 낮춤.
주방장이 요리사(객체)를 관리하고, 필요한 재료(의존성)를 전달하여 효율적으로 관리.
6. 결론
IoC는 **"객체의 제어권을 개발자가 아닌 Spring 컨테이너에 위임"**하는 철학.
DI는 **"객체 간의 의존성을 외부에서 주입"**하여 유연성과 테스트 용이성을 높이는 구현 방식.
Spring은 IoC 컨테이너와 DI를 사용해 객체 생성을 관리하고, 의존성을 주입하여 효율적이고 유연한 애플리케이션 설계를 가능하게 합니다! 😊
Spring AOP는 **Aspect-Oriented Programming(AOP)**의 개념을 Spring Framework에 적용한 구현체로, **횡단 관심사(Cross-Cutting Concerns)**[ 여러 곳에서 공통적으로 필요한 기능 ]를 깔끔하게 관리하고, 핵심 비즈니스 로직과 보조 로직을 분리할 수 있게 도와줍니다.
= AOP란 공통 기능을 한 곳에 정의하고 필요한 코드에 자동으로 적용하는 방법
Spring AOP는 프록시(Proxy) 기반으로 동작하며, 주로 메서드 실행에 Advice를 적용합니다.
스프링 컨테이너가 **스프링 빈(Spring Bean)**을 관리한다는 것은 빈의 생성, 초기화, 의존성 주입, 스코프 관리, 그리고 소멸까지의 전체 생명 주기를 관리한다는 것을 의미합니다. 관리 과정은 다음과 같은 방식으로 이루어집니다.
1. 스프링 빈의 관리 과정 (생명 주기)
스프링 컨테이너가 스프링 빈을 관리하는 과정은 다음 단계를 따릅니다:
1) 빈 정의 (Bean Definition):
개발자는 어노테이션(@Component, @Service, @Controller 등)이나 XML/Java 설정 파일을 통해 빈을 정의합니다.
스프링 컨테이너는 애플리케이션 시작 시 이 정의를 읽어들여 빈 생성 규칙을 설정합니다.
2) 빈 생성 (Bean Creation):
스프링 컨테이너는 빈 정의에 따라 빈 객체를 생성합니다.
기본적으로 **싱글톤(Singleton)**으로 관리되며, 애플리케이션 컨텍스트 초기화 시 생성됩니다.
3) 의존성 주입 (Dependency Injection):
빈이 생성된 후, 스프링 컨테이너는 해당 빈이 필요로 하는 의존성을 주입합니다.
생성자 주입, 세터 주입, 필드 주입 중 하나의 방식으로 주입됩니다.
예: @Autowired, @Value, @Qualifier 등을 사용.
4) 초기화 (Initialization):
빈 생성 후 초기화 메서드(예: @PostConstruct 또는 init-method)를 호출하여 필요한 설정을 수행합니다.
개발자가 정의한 초기화 로직이 실행됩니다.
5) 사용 (Usage):
빈은 애플리케이션 내에서 사용되며, 컨테이너가 이를 제공하고 호출하는 역할을 합니다.
6) 소멸 (Destruction):
애플리케이션 종료 시, 컨테이너는 빈을 제거하기 전에 소멸 메서드(예: @PreDestroy 또는 destroy-method)를 호출합니다.
2. 스프링 빈의 관리 요소
1) 빈 스코프 관리
스프링 빈은 **스코프(Scope)**를 통해 객체의 생명 주기를 정의합니다. 빈의 스코프는 객체가 생성되고 유지되는 범위를 의미합니다.
싱글톤(Singleton):
컨테이너 당 하나의 인스턴스만 생성 (기본값).
모든 요청에서 같은 객체를 공유합니다.
프로토타입(Prototype):
요청할 때마다 새로운 객체 생성.
요청(Request):
웹 애플리케이션에서 HTTP 요청마다 새로운 빈 생성.
세션(Session):
웹 애플리케이션에서 HTTP 세션마다 새로운 빈 생성.
2) 의존성 관리
스프링 컨테이너는 빈이 필요로 하는 의존성을 자동으로 관리하고 주입합니다.
예제: 생성자 주입
@Component
class NotificationService {
private final MessageService messageService;
@Autowired
public NotificationService(MessageService messageService) {
this.messageService = messageService; // 의존성 주입
}
public void notify(String message) {
messageService.sendMessage(message);
}
}
예제: 세터 주입
@Component
class NotificationService {
private MessageService messageService;
@Autowired
public void setMessageService(MessageService messageService) {
this.messageService = messageService; // 의존성 주입
}
public void notify(String message) {
messageService.sendMessage(message);
}
}
3. 코드 예제로 이해
스프링 빈의 생명 주기
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
@Component
class ExampleBean implements InitializingBean, DisposableBean {
// 빈 생성자
public ExampleBean() {
System.out.println("1. 빈 생성");
}
// 의존성 주입 후 호출
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("2. 빈 초기화");
}
// 빈 소멸 전 호출
@Override
public void destroy() throws Exception {
System.out.println("3. 빈 소멸");
}
}
// 메인 클래스
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
// 컨테이너 생성 및 빈 관리 시작
var context = new AnnotationConfigApplicationContext(AppConfig.class);
// ExampleBean 사용
ExampleBean exampleBean = context.getBean(ExampleBean.class);
// 컨테이너 종료 및 빈 소멸
context.close();
}
}
실행 결과:
1. 빈 생성
2. 빈 초기화
3. 빈 소멸
4. 일상적인 비유
스프링 컨테이너와 빈 관리의 비유
스프링 컨테이너: 레스토랑의 매니저
메뉴와 재료를 정의하고, 주방에서 요리가 준비되도록 관리.
각 요리(빈)를 주문받아 제공.
스프링 빈: 레스토랑의 요리
매니저가 주방에서 요리를 준비하고, 고객이 주문하면 제공합니다.
요청마다 같은 요리를 재사용할 수도 있고(싱글톤), 매번 새롭게 준비할 수도 있습니다(프로토타입).
5. 정리 표
스프링 컨테이너
스프링 빈
정의
객체(스프링 빈)의 생성, 초기화, 의존성 주입, 생명 주기를 관리하는 도구.
스프링 컨테이너에 의해 관리되는 객체.
역할
빈 생성, 의존성 주입, 스코프 관리, 생명 주기 관리.
실제 애플리케이션에서 사용되는 객체.
생성 시점
애플리케이션 시작 시 초기화 (스프링 컨테이너 초기화).
스프링 컨테이너에 의해 정의된 설정에 따라 생성.
소멸 시점
애플리케이션 종료 시 빈 소멸.
빈의 소멸 메서드 호출 후 컨테이너에서 제거.
관리 범위
애플리케이션 전체의 객체 관리.
개별 빈 단위 관리.
스코프
싱글톤, 프로토타입, 요청, 세션 등 관리.
컨테이너의 설정에 따라 하나의 객체 또는 여러 객체로 생성.
6. 결론
스프링 컨테이너는 빈의 생성부터 소멸까지 전체 생명 주기를 관리하며, 개발자는 객체 생성과 의존성 설정에 대한 걱정을 줄이고 비즈니스 로직에 집중할 수 있습니다.
스프링 빈은 컨테이너에서 관리되는 객체로, 요청에 따라 필요한 의존성을 주입받고, 컨테이너의 설정에 따라 재사용되거나 새로 생성됩니다.
스프링의 이러한 관리 방식은 객체 간 결합도를 줄이고, 코드의 유연성과 유지보수성을 높이는 데 큰 역할을 합니다.
3. 스프링 컨테이너와 스프링 빈의 관계
스프링 컨테이너는 스프링 빈을 관리하는 도구입니다.
개발자는 스프링 컨테이너에 객체(빈)를 등록하고, 컨테이너는 이 빈을 생성하고 관리합니다.
스프링 컨테이너는 애플리케이션 실행 시 빈을 생성하고, 의존성을 주입하며, 생명 주기를 관리합니다.
4. 일상적인 예시
1) 스프링 컨테이너와 스프링 빈
스프링 컨테이너: **관리인(컨테이너)**이 빵집의 모든 직원과 재료를 관리.
스프링 빈: 관리인이 관리하는 **직원(객체)**과 재료(의존성).
비유
빵집의 모든 직원은 관리인의 관리하에 빵을 만듭니다.
직원들 간의 의존성(예: 반죽 -> 오븐 -> 포장)은 관리인이 연결해줍니다.
관리인이 직원의 고용, 교체, 해고(생명 주기)를 관리합니다.
5. 코드 예제
1) 스프링 컨테이너와 빈 등록
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Component;
// 메시지 서비스 인터페이스
interface MessageService {
void sendMessage(String message);
}
// EmailService 구현체
@Component
class EmailService implements MessageService {
public void sendMessage(String message) {
System.out.println("Sending Email: " + message);
}
}
// NotificationService 클래스
@Component
class NotificationService {
private final MessageService messageService;
// 생성자를 통한 의존성 주입
public NotificationService(MessageService messageService) {
this.messageService = messageService;
}
public void notify(String message) {
messageService.sendMessage(message);
}
}
// 스프링 설정 클래스
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.example")
class AppConfig {
}
// Main 클래스
public class Main {
public static void main(String[] args) {
// 스프링 컨테이너 생성
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 스프링 컨테이너에서 NotificationService 빈 가져오기
NotificationService notificationService = context.getBean(NotificationService.class);
notificationService.notify("Hello, Spring!");
}
}
Spring Boot는 application.properties 또는 application.yml 파일을 사용해 간단하게 애플리케이션의 설정을 관리할 수 있습니다.
서버의 자동 실행
내장 웹 서버를 통해 별도의 설정 없이, 애플리케이션을 java -jar 명령어로 실행할 수 있습니다.
개발자 친화적인 환경
Spring Boot는 애플리케이션의 개발과 디버깅을 빠르게 할 수 있도록DevTools라는 기능을 제공하며, 코드 변경 시 자동으로 애플리케이션을 재시작하여 개발 편의성을 제공합니다.
[2] Spring Boot의 장점
빠른 시작:
Spring Boot는 자동 설정 및 기본적인 템플릿을 제공하여, 애플리케이션을 몇 가지 설정만으로 빠르게 시작할 수 있습니다. @SpringBootApplication 어노테이션을 추가하면, 기본적인 설정이 자동으로 이루어지고, 애플리케이션을 실행할 수 있는 상태가 됩니다.
설정 최소화:
Spring Boot는 많은 설정을 자동으로 처리하므로 개발자는 비즈니스 로직에만 집중할 수 있습니다. 예를 들어, 데이터베이스 설정, 서버 설정등 대부분의 설정을 자동으로 처리하여 개발자가 별도로 신경 쓸 필요가 없습니다.
내장 서버:
내장 서버를 제공하여 별도의 외부 웹 서버(Tomcat, Jetty 등이 내장되어 있다.)를 설치할 필요 없이 바로 실행할 수 있습니다. 이는 애플리케이션의 배포를 간소화하고, 실행 파일 하나로 애플리케이션을 배포할 수 있게 만듭니다.
생산성 향상:
Spring Boot는 개발자가 빠르게 단독으로 실행할 수 있는 애플리케이션을 작성하고 실행할 수 있도록 돕기 위해 많은스타터 의존성을 제공합니다. 예를 들어, 웹 애플리케이션을 만들기 위한 spring-boot-starter-web, 데이터베이스 연동을 위한 spring-boot-starter-data-jpa 등이 있습니다.
DevTools:
Spring Boot는 개발 중에자동 재시작및Hot swapping을 지원하여, 개발자가 코드 수정 후 애플리케이션을 다시 시작하지 않고도 변경 사항을 바로 반영할 수 있도록 돕습니다.
[?] 특징, 장점은 여러개가 있지만 결국 왜 스프링을 사용할까?
= 자바의 가장 큰 특징은 객체 지향이다.스프링은 좋은 객체 지향 애플리케이션 개발을 도와주는 프레임워크다.
[3] Spring Boot 애플리케이션 구조
애플리케이션 클래스
@SpringBootApplication 어노테이션이 붙은 클래스는 Spring Boot 애플리케이션의 진입점입니다. 이 클래스는 자동으로 필요한 설정을 수행하고 애플리케이션을 실행합니다.
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
자동 설정:
Spring Boot는 애플리케이션을 실행할 때, 주어진 환경에 맞춰 자동으로 설정을 적용합니다. 예를 들어, 데이터베이스가 설정되면 자동으로 DataSource와 관련된 설정이 적용됩니다.
프로퍼티 파일:
Spring Boot는 application.properties 또는 application.yml 파일을 통해 애플리케이션의 설정을 관리할 수 있습니다. 데이터베이스 연결 정보, 서버 포트, 로깅 수준 등 다양한 설정을 이 파일에서 처리합니다.
내장 서버:
Spring Boot는 기본적으로 내장된 Tomcat 서버를 포함하고 있으며, 설정에 따라 다른 내장 서버(Undertow, Jetty 등)를 사용할 수 있습니다.
[4] Spring Boot 애플리케이션 실행
Maven 또는 Gradle을 사용한 빌드 후 실행:
애플리케이션을 빌드하고 실행하려면, mvn spring-boot:run 또는 gradle bootRun 명령을 사용할 수 있습니다.
JAR 파일로 실행:
Spring Boot 애플리케이션을 JAR 파일로 빌드한 후, java -jar 명령어로 실행할 수 있습니다.
java -jar myapp.jar
[5] Spring Boot 사용 예시
간단한 RESTful 웹 서비스:
@RestController
@RequestMapping("/api")
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
}
자동 설정 예시: Spring Boot는 데이터베이스 연결을 자동으로 처리할 수 있습니다. 예를 들어, 데이터베이스 설정을 application.properties에서 지정하면, Spring Boot는 이를 자동으로 인식하고 설정을 완료합니다.
스프링은 DefaultConversionService를 통해 다양한 타입 변환을 기본 제공하며, 이를 직접 사용할 수 있습니다.
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.ConversionService;
public class Main {
public static void main(String[] args) {
// DefaultConversionService 생성
ConversionService conversionService = new DefaultConversionService();
// 기본 변환 사용
int convertedInt = conversionService.convert("123", Integer.class);
boolean convertedBool = conversionService.convert("true", Boolean.class);
// 결과 출력
System.out.println("Converted Integer: " + convertedInt); // 123
System.out.println("Converted Boolean: " + convertedBool); // true
}
}
2) 커스텀 변환기 등록
1. 커스텀 변환기 작성
import org.springframework.core.convert.converter.Converter;
// String -> CustomType 변환기
public class StringToCustomTypeConverter implements Converter<String, CustomType> {
@Override
public CustomType convert(String source) {
return new CustomType(source);
}
}
// CustomType 클래스
public class CustomType {
private final String value;
public CustomType(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
2. Custom ConversionService 설정
import org.springframework.core.convert.support.GenericConversionService;
public class Main {
public static void main(String[] args) {
// ConversionService 생성
GenericConversionService conversionService = new GenericConversionService();
// 커스텀 변환기 등록
conversionService.addConverter(new StringToCustomTypeConverter());
// 변환 수행
CustomType customType = conversionService.convert("test-value", CustomType.class);
// 결과 출력
System.out.println("Converted CustomType Value: " + customType.getValue());
}
}
4. ConversionService와 FormatterRegistry
**ConversionService**는 변환기(Converter)를 관리하며, 타입 변환을 처리합니다.
**FormatterRegistry**는 포맷터(Formatter)를 지원하며, ConversionService의 확장된 형태로 볼 수 있습니다.
WebMvcConfigurer에서 FormatterRegistry를 사용하여 변환기나 포맷터를 등록할 수 있습니다.
변환기 등록
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToCustomTypeConverter());
}
}
**필터(Filter)**는 클라이언트의 요청(request)와 서버의 응답(response) 사이에서 요청 처리 전/후에 특정 작업을 수행할 수 있는 기능을 제공합니다. 필터는 주로 HTTP 요청과 응답을 가로채어 조작하거나, 요청이 컨트롤러에 도달하기 전에 사전 작업을 수행하는 데 사용됩니다.
2. 필터의 주요 사용 사례
인증 및 권한 검사:
요청이 적합한 사용자로부터 왔는지 확인하거나, 요청의 권한을 검증.
로깅 및 모니터링:
요청 및 응답 데이터를 기록하여 애플리케이션의 동작을 추적.
데이터 변환/압축:
요청 데이터 변환 또는 응답 데이터 압축.
CORS 처리:
Cross-Origin Resource Sharing(CORS) 요청 처리.
보안 작업:
CSRF(Cross-Site Request Forgery) 방지 토큰 검증, 헤더 조작 방지 등.
스프링이 제공하는 Filter 인터페이스는 서블릿 표준(javax.servlet.Filter)을 확장한 것입니다.
전역적으로 동작:
컨트롤러에 도달하기 전에 모든 요청/응답에 대해 동작합니다.
체인 방식:
여러 필터가 등록되어 있는 경우, 필터 체인(Filter Chain)을 통해 순서대로 실행됩니다.
4. 필터 구현 방법
1) 기본 필터 구현
javax.servlet.Filter 인터페이스를 구현하여 필터를 작성합니다.
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
// 모든 요청에 대해 필터 적용
@WebFilter("/*")
public class LoggingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 초기화 작업 (필요 시 구현)
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 요청 처리 전
System.out.println("Request received at: " + request.getRemoteAddr());
// 다음 필터 또는 서블릿 호출
chain.doFilter(request, response);
// 응답 처리 후
System.out.println("Response processed");
}
@Override
public void destroy() {
// 리소스 해제 작업 (필요 시 구현)
}
}
doFilter 메서드:
요청이 컨트롤러로 전달되기 전, 특정 작업을 수행합니다.
chain.doFilter(request, response)를 호출하여 다음 필터 또는 서블릿으로 요청을 전달합니다.
chain.doFilter 이후 코드는 응답이 생성된 후 실행됩니다.
2) 스프링 부트에서 필터 등록
스프링 부트에서는 @Bean을 사용해 필터를 등록할 수 있습니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
@Configuration
public class FilterConfig {
@Bean
public Filter loggingFilter() {
return new LoggingFilter();
}
}
5. 필터 체인(Filter Chain)
필터는 여러 개 등록될 수 있으며, 등록 순서에 따라 체인 방식으로 동작합니다.
필터가 호출될 때, FilterChain을 통해 다음 필터로 요청을 전달합니다.
최종적으로 컨트롤러나 서블릿에 도달하며, 응답은 필터 체인을 역순으로 통과합니다.
6. 필터의 실행 순서 지정
스프링 부트에서는 @Order 또는 FilterRegistrationBean을 사용해 필터의 실행 순서를 지정할 수 있습니다.
1) @Order로 순서 지정
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(1) // 낮은 숫자가 먼저 실행됨
public class FirstFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("First Filter - Before");
chain.doFilter(request, response);
System.out.println("First Filter - After");
}
}
2) FilterRegistrationBean으로 순서 지정
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<LoggingFilter> loggingFilter() {
FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new LoggingFilter());
registrationBean.setOrder(2); // 순서 지정 (낮은 숫자가 먼저 실행)
registrationBean.addUrlPatterns("/*"); // 필터를 적용할 URL 패턴
return registrationBean;
}
}
@Bean
public FilterRegistrationBean<JwtAuthFilter> jwtAuthFilter() {
FilterRegistrationBean<JwtAuthFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new JwtAuthFilter());
registrationBean.addUrlPatterns("/secure/*"); // 특정 URL 패턴에만 적용
registrationBean.setOrder(1);
return registrationBean;
}
8. 정리
항목
설명
정의
클라이언트 요청과 서버 응답 사이에서 특정 작업을 수행하는 기능.
주요 사용 사례
인증/권한, 로깅, 요청/응답 데이터 변환, CORS 처리, 보안 검사.
동작 방식
요청 → 필터 체인 → 컨트롤러 → 응답 (필터 체인 역순으로 실행).
등록 방법
1. @WebFilter2. FilterRegistrationBean3. @Bean.
순서 지정 방법
@Order 또는 FilterRegistrationBean의 setOrder 메서드.
장점
전역적으로 요청/응답 처리 가능, 공통 로직 분리, 강력한 조작 기능 제공.
단점
컨트롤러 단위로 작동하지 않으므로 세부 로직 구현 시 코드가 복잡해질 수 있음.
9. 결론
필터는 HTTP 요청과 응답을 처리하는 강력한 전처리/후처리 메커니즘으로, 인증, 로깅, 데이터 조작 등 다양한 작업을 수행할 수 있습니다. 스프링 부트에서는 간단한 설정으로 필터를 등록하고, 순서를 지정하여 유연하게 요청/응답 처리를 관리할 수 있습니다. 필터는 전역적으로 동작하기 때문에, 컨트롤러 단위보다 요청 흐름의 초기 단계에서 작업을 수행할 때 적합합니다.
API 예외처리
API 예외처리는 클라이언트와 서버 간의 상호작용 중 발생할 수 있는 오류를 처리하고, 클라이언트에 일관된 오류 응답을 제공하기 위한 방법입니다.
1. API 예외처리의 목표
일관성:
클라이언트가 API의 오류를 쉽게 이해하고 처리할 수 있도록, 표준화된 형식의 오류 응답 제공.
가독성:
명확한 오류 메시지와 상태 코드 전달로 디버깅과 문제 해결을 간소화.
보안:
민감한 서버 내부 정보를 클라이언트에 노출하지 않음.
2. 기본적인 예외 처리 방식
1) @ExceptionHandler 사용
특정 컨트롤러에서 발생한 예외를 처리하기 위한 메서드를 지정합니다.
단일 컨트롤러 내에서 예외를 처리한 후
계층별로 알맞은 예외를 발생(throw new)시키기만 하면됩니다.
스프링 MVC에서 컨트롤러 단위로 예외를 처리할 수 있습니다.
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.ResponseEntity;
@RestController
public class MyController {
@GetMapping("/example")
public String example() {
if (true) {
throw new IllegalArgumentException("Invalid input!");
}
return "Success";
}
// 특정 예외 처리
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e) {
return ResponseEntity.badRequest().body("Error: " + e.getMessage());
}
}
결과:
요청: GET /example
응답:
{
"error": "Error: Invalid input!"
}
문제점
Controller 코드에 Exception 처리를 위한 책임이 추가된다.(단일 책임 원칙 위반)
단일 컨트롤러 내의 예외만 처리가 가능하다.(컨트롤러 예외처리 중복코드)
코드 재사용, 유지보수성 부족
CustomException사용자 정의Exception을 만들어서 처리할 수 있다.
2) @ControllerAdvice 사용
@ControllerAdvice는 애플리케이션 전역에서 예외를 처리할 수 있도록 해줍니다. 컨트롤러마다 예외 처리 로직을 반복하지 않아도 됩니다.
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Error: " + e.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGeneralException(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Unexpected error occurred!");
}
}
결과:
모든 컨트롤러에서 발생하는 예외를 처리할 수 있습니다.
예외마다 별도의 처리 로직을 정의할 수 있습니다.
3) ResponseEntityExceptionHandler 상속
스프링의 기본 예외 처리 기능을 확장하여 API 예외 처리를 커스터마이징할 수 있습니다.
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body,
HttpHeaders headers, HttpStatus status, WebRequest request) {
return new ResponseEntity<>(new ErrorResponse("INTERNAL_ERROR", ex.getMessage()), status);
}
// 사용자 정의 예외 처리
@ExceptionHandler(CustomException.class)
public ResponseEntity<Object> handleCustomException(CustomException ex) {
return new ResponseEntity<>(new ErrorResponse("CUSTOM_ERROR", ex.getMessage()), HttpStatus.BAD_REQUEST);
}
}
class ErrorResponse {
private String code;
private String message;
public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
}
// Getters and setters
}
3. 응답 형식 표준화
JSON 형식의 응답을 사용하여 클라이언트가 오류를 쉽게 이해하도록 일관된 구조를 제공합니다.
📌 객체 간의 의존성을 스프링 컨테이너가 자동으로 관리하고 주입해주는 설계 패턴입니다. 객체가 다른 객체를 필요로 할 때, 직접 생성하지 않고 외부에서 주입받아 사용하도록 설계하는 방식입니다.
@Autowired 는 의존성을 자동으로 주입할 때 사용하는 Annotation 이다.
기본적으로 주입할 대상이 없으면 오류가 발생한다.(required = true)
의존관계 주입의 기본 개념
의존성(Dependency):
A 객체가 B 객체를 사용해야 한다면, A는 B에 의존하고 있다고 말합니다.
이때 B 객체를 A 내부에서 직접 생성하지 않고,외부에서 주입받는 것을 의존관계 주입이라고 합니다.
스프링 DI:
스프링 컨테이너가 객체(빈)를 생성하고 관리하면서 필요한 의존성을 자동으로 주입합니다.
개발자는 직접 의존성을 생성하거나 연결할 필요가 없으며, 컨테이너가 이를 처리합니다.
왜 의존성 주입인가?
@Autowired와 스프링 컨테이너가 있기 때문에, 개발자가 의존 객체를 직접 생성하지 않아도 되고, 컨테이너가 이를 대신 처리합니다. 이 점이 의존성 주입의 핵심입니다.
장점
결합도 감소:
MyApp은 MyService의 구체적인 구현체를 몰라도 됩니다. (DIP 원칙 준수)
다른 구현체로 변경할 때 코드 수정이 필요 없습니다.
유연성 증가:
스프링 컨테이너에서 주입받는 객체를 쉽게 교체하거나 확장할 수 있습니다.
테스트 용이성:
테스트 환경에서 Mock 객체를 주입할 수 있습니다.
1. 생성자 주입
생성자를 통해 의존성을 주입하는 방법.
최초에 한번 생성된 후 값이 수정되지 못한다.[불변, 필수]
public interface MyService {
void doSomething();
}
// Spring Bean으로 등록
@Service
public class MyServiceImpl implements MyService {
@Override
public void doSomething() {
System.out.println("MyServiceImpl 메서드 호출");
}
}
// 생성자 주입 방식
@Component
public class MyApp {
// 필드에 final 키워드 필수! 무조건 값이 있도록 만들어준다.(필수)
private final MyService myService;
// 생성자를 통해 의존성 주입, 생략 가능
@Autowired
public MyApp(MyService myService) {
this.myService = myService;
}
public void run() {
myService.doSomething();
}
}
@ComponentScan(basePackages = "com.example.springdependency.test")
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
// 등록된 MyApp 빈 가져오기
MyApp myApp = context.getBean(MyApp.class);
// 빈 메서드 호출
myApp.run();
}
}
-------------------------------------
// 생성자가 두개인 경우 생략이 불가능하다.
@Component
public class MyApp {
// 필드에 final 키워드 필수! 무조건 값이 있도록 만들어준다.(필수)
private final MyService myService;
public MyApp(MyService myService, String myRepository) {
this.myService = myService;
}
// 생성자를 통해 의존성 주입
// @Autowired를 생략하기 위해서는 생성자가 하나여야 한다.
public MyApp(MyService myService) {
this.myService = myService;
}
public void run() {
service.doSomething();
}
}
생성자가 하나인 경우 @Autowired 생략이 가능하다.
둘중 어떤 생성자를 사용해야 하는지 Spring은 알지 못한다.
2. Setter 주입
Setter 메서드를 통해 의존성을 주입하는 방법.
@Component
public class MyApp {
private MyService myService;
// Setter 주입
@Autowired
public void setMyService(MyService myService) {
this.myService = myService;
}
public void run() {
myService.doSomething();
}
}
선택하거나, 변경 가능한 의존관계에 사용한다.(생성자 주입은 필수 값)
// MyService가 Spring Bean으로 등록되지 않은 경우에도 주입이 가능하다.
@Autowired(required = false)
public void setMyService(MyService myService) {
this.myService = myService;
}
// 실행 도중 인스턴스를 바꾸고자 하는 경우
// setMyService(); 메서드를 외부에서 호출하면 된다.(이런 경우는 거의 없음)
3. 필드 주입
필드에 직접적으로 주입하는 방법 (가장 추천되지 않음).
@Component
public class MyApp {
@Autowired
private MyService myService; // 필드에 직접 주입
public void run() {
myService.doSomething();
}
}
코드는 간결하지만 Spring이 없으면 사용할 수 없다.
사용하지 않아야 한다.
// Spring을 사용하지 않는 경우 실행이 불가능하다.
public class MainV2 {
public static void main(String[] args) {
MyApp myApp = new MyApp();
myApp.run();
}
}
외부에서 myService 값을 설정하거나 변경할 방법이 없다.
결국 setter를 만들어야 한다.
순수 Java 코드로 사용할 수 없다. = 테스트 코드 작성이 힘들다.
Application의 실행과 관계 없는 @SpringBootTest 테스트 코드나 Spring에서만 사용하는 @Configuration 같은 곳에서 주입할 때 주로 사용한다.
4. 일반 메서드 주입
생성자, setter 주입으로 대체가 가능하기 때문에 사용하지 않는다.
@Component
public class MyApp {
private MyService myService;
// 일반 메서드 주입
@Autowired
public void init(MyService myService) {
this.myService = myService;
}
public void run() {
myService.doSomething();
}
}
의존관계를 자동으로 주입할 객체가 Spring Bean으로 등록되어 있어야 @Autowired 로 주입이 가능하다.
생성자 주입
📌 과거 setter, 필드 주입도 사용했지만 현재는 DI를 가지고 있는 대부분의 Framework가 생성자 주입 방식을 권장한다.
생성자 주입을 선택하는 이유
불변(immutable)
어떤 요리(Web Application)를 만들지 정해졌다면 이미 재료(Bean)와 의존 관계가 결정된다.
객체를 생성할 때 최초 한번만 호출된다.(불변)
setter 주입을 사용하면 접근제어자가 public 으로 설정되어 누구나 수정할 수 있게된다.
실수 방지
순수 Java 코드로 사용할 때(주로 테스트 코드) 생성자의 필드를 필수로 입력하도록 만들어준다.(NPE 방지)
컴파일 시점에 오류를 발생 시킨다. 즉, 실행 전에 오류를 알 수 있다.
public class MyApp {
private MyService myService;
public MyApp() {
this.myService = new MyService(); // 직접 생성
}
public void run() {
myService.doSomething();
}
}
---------------------------------------
@Component // 해당 어노테이션이 MyService 객체를 bean으로 만들어준다. 즉 스프링이 객체 MyService를 저장한다.
public class MyApp {
private final MyService myService;
@Autowired
public MyApp(MyService myService) {
this.myService = myService; // 외부에서 주입 (직접 선언이 아니라 스프링에 저장된 객체 사용)
}
public void run() {
myService.doSomething();
}
}
위 코드 참고
@Autowired: 스프링 컨테이너가 MyService 타입의 빈을 찾아서 자동으로 주입합니다.
MyApp은 MyService를 직접 생성하지 않습니다. 대신, 스프링 컨테이너가 제공한 인스턴스를 사용합니다.
Spring Framework에 의존하지 않아도 객체 지향 특성을 가장 잘 사용하는 방법이다.
필드에 final 은 생성자 주입 방식만 사용할 수 있다. 나머지 주입 방식들은 모두 생성 이후에 호출되어 사용할 수 없다.
@RequiredArgsConstructor
📌 실제 Web Application을 개발하면 대부분이 불변 객체이고 생성자 주입 방식을 선택하게 된다. 이런 반복되는 코드를 편안하게 작성하기 위해 Lombok에서 제공하는 Annotation 이다.
@RequiredArgsConstructor
final 필드를 모아서 생성자를 자동으로 만들어 주는 역할
Annotation Processor 가 동작하며 컴파일 시점에 자동으로 생성자 코드를 만들어준다.
사용 방법
@Component
@RequiredArgsConstructor
public class MyApp {
// 필드에 final 키워드 필수! 무조건 값이 있도록 만들어준다.(필수)
private final MyService myService;
// Annotation Processor가 만들어 주는 코드
// public MyApp(MyService myService) {
// this.myService = myService;
// }
public void run() {
myService.doSomething();
}
}
📌 Spring이 특정 패키지 내에서 @Component, @Service, @Repository, @Controller같은 Annotation이 붙은 클래스를 자동으로 검색하고, 이를 Bean으로 등록하는 기능이다. 개발자가 Bean을 직접 등록하지 않고도 Spring이 자동으로 관리할 객체들을 찾는다.
ComponentScan의 역할
Chef가 요리할 재료를 자동으로 식료품 저장고에서 찾아오는 과정, Chef는 스스로 필요한 재료를 찾아 요리에 사용한다.
요리사(개발자)가 직접 재료(Bean)를 찾아서 가져올 필요가 없다.
@ComponentScan
특정 패키지 내에 @Component Annotation이 붙은 클래스를 자동으로 찾아서 Spring Bean으로 등록한다.
Annotation을 이용해 Bean을 등록할 수 있어 코드가 간결해지고 유지보수가 쉬워진다.
스캐닝 범위는 주로 애플리케이션의 루트(최상위) 패키지에서 시작된다.
@SpringBootApplication
스프링 부트 애플리케이션의 시작점을 정의하기 위해 사용하는 애너테이션입니다. 이 애너테이션은 여러 기능을 결합한 복합 애너테이션으로, 스프링 부트 애플리케이션을 간단히 설정하고 실행할 수 있도록 돕습니다.
SpringBoot로 프로젝트를 생성하면 main() 메서드가 있는 클래스 상단에 @SpringBootApplication Annotation 이 존재한다.
@ComponentScan의 속성
basePackages: 특정 패키지를 스캔할 때 사용, 배열로 여러개를 선언할 수 있다.
Spring Application이 실행되면 @ComponentScan이 지정된 패키지를 탐색한다.
@ComponentScan은 @Component, @Service, @Repository, @Controller 등의 애너테이션이 붙은 클래스를 탐색합니다.
해당 클래스들을 스프링 빈으로 등록합니다.
구현 클래스뿐만 아니라, 인터페이스와 그 구현체도 함께 빈으로 등록됩니다.
2. 의존성 정의 (인터페이스 기반 설계)
일반적으로 **인터페이스(추상화)**를 의존성으로 정의합니다. 구현체는 스프링이 자동으로 선택하고 주입합니다.
3. 구현체 자동 연결
스프링은 등록된 빈 중에서 의존성으로 선언된 인터페이스에 맞는 구현체를 자동으로 찾아 주입합니다.
이 과정은 타입 매칭을 통해 이루어집니다.
@Configuration, @Bean
📌 Spring Bean을 등록하는 방법에는 수동, 자동 두가지가 존재한다.
Spring Bean 등록 방법
Spring Bean은 Bean의 이름으로 등록된다.
1. 자동 Bean 등록(@ComponentScan, @Component)
@Component 이 있는 클래스의 앞글자만 소문자로 변경하여 Bean 이름으로 등록한다.
// myService 라는 이름의 Spring Bean
@Component
public class MyService {
public void doSomething() {
System.out.println("Spring Bean 으로 동작");
}
}
@ComponentScan 을 통해 @Component로 설정된 클래스를 찾는다.
2. 수동 Bean 등록(@Configuration, @Bean)
@Configuration 이 있는 클래스를 Bean으로 등록하고 해당 클래스를 파싱해서 @Bean 이 있는 메서드를 찾아 Bean을 생성한다. 이때 해당 메서드의 이름으로 Bean의 이름이 설정된다.
// 인터페이스
public interface TestService {
void doSomething();
}
// 인터페이스 구현체
public class TestServiceImpl implements TestService {
@Override
public void doSomething() {
System.out.println("Test Service 메서드 호출");
}
}
// 수동으로 빈 등록
@Configuration
public class AppConfig {
// TestService 타입의 Spring Bean 등록
@Bean
public TestService testService() {
// TestServiceImpl을 Bean으로 등록
return new TestServiceImpl();
}
}
// Spring Bean으로 등록이 되었는지 확인
public class MainApp {
public static void main(String[] args) {
// Spring ApplicationContext 생성 및 설정 클래스(AppConfig) 등록
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 등록된 TestService 빈 가져오기
TestService service = context.getBean(TestService.class);
// 빈 메서드 호출
service.doSomething();
}
}
수동으로 Bean을 등록할 때는 항상@Configuration과 함께 사용해야 Bean이 싱글톤으로 관리된다. CGLIB 라이브러리와 연관이 있다.
**@Configuration**은 스프링에서 Java Config 클래스를 정의할 때 사용하는 애너테이션입니다. 이 애너테이션을 붙인 클래스는 스프링 컨테이너가 관리하는 설정 클래스로 동작하며, 해당 클래스에 정의된 Bean은 싱글톤으로 관리됩니다.
싱글톤 관리:
@Configuration이 붙은 클래스는 내부적으로 CGLIB 동적 프록시 객체로 변환됩니다.
이 프록시 객체는 @Bean 메서드 호출 시, 이미 생성된 Bean이 있으면 이를 반환하고, 없으면 새로운 Bean을 생성합니다.
이를 통해 동일한 Bean이 여러 번 생성되지 않도록 보장합니다.
Bean 충돌
📌 Bean 등록 방법에는 수동, 자동 두가지가 존재하고 Bean은 각각의 이름으로 생성된다. 이때 이름이 같은 Bean이 설정되고자 한다면 충돌이 발생한다.
같은 이름의 Bean 등록
자동 Bean 등록 VS 자동 Bean 등록
public interface ConflictService {
void test();
}
// Bean의 이름을 service로 설정
@Component("service")
public class ConflictServiceV1 implements ConflictService {
@Override
public void test() {
System.out.println("Conflict V1");
}
}
// Bean의 이름을 service로 설정
@Component("service")
public class ConflictServiceV2 implements ConflictService {
@Override
public void test() {
System.out.println("Conflict V2");
}
}
// componentScan의 범위를 conflict 패키지 하위로 설정
@ComponentScan(basePackages = "com.example.springconcept.conflict")
public class ConflictApp {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(ConflictApp.class);
// Service 빈을 가져와서 실행
ConflictService service = context.getBean(ConflictService.class);
service.test();
}
}
ConflictingBeanDefinitionException 발생
수동 Bean 등록 VS 자동 Bean 등록
// conflictService 이름으로 Bean 생성
@Component
public class ConflictService implements MyService {
@Override
public void doSomething() {
System.out.println("ConflictService 메서드 호출");
}
}
public class ConflictServiceV2 implements MyService {
@Override
public void doSomething() {
System.out.println("ConflictServiceV2 메서드 호출");
}
}
// 수동으로 Bean 등록
@Configuration
public class ConflictAppConfig {
// conflictService 이름으로 Bean 생성
@Bean(name = "conflictService")
MyService myService() {
return new ConflictServiceV2();
}
}
@ComponentScan(basePackages = "com.example.springconcept.conflict2")
public class ConflictApp2 {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(ConflictApp2.class);
// Service 빈을 가져와서 실행
MyService service = context.getBean(MyService.class);
service.doSomething();
}
}
수동 Bean 등록이 자동 Bean 등록을 오버라이딩해서 우선권을 가진다.
의도한 결과라면 다행이지만, 아닌 경우(실수)가 대부분이다. → 버그 발생
Spring Boot에서는 수동과 자동 Bean등록의 충돌이 발생하면 오류가 발생한다.
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
// 수동, 자동 Bean을 동시에 등록할 때 이름이 같으면 수동 Bean이 오버라이딩
spring.main.allow-bean-definition-overriding=true
// 기본값
spring.main.allow-bean-definition-overriding=false
@Qualifier, @Primary
📌 같은 타입의 Bean이 중복된 경우 해결하기 위해 사용하는 Annotation
같은 타입의 Bean 충돌 해결 방법
@Autowired + 필드명 사용
@Autowired 는 타입으로 먼저 주입을 시도하고 같은 타입의 Bean이 여러개라면 필드 이름 혹은 파라미터 이름으로 매칭한다.
public interface MyService { ... }
@Component
public class MyServiceImplV1 implements MyService { ... }
@Component
public class MyServiceImplV2 implements MyService { ... }
@Component
public class ConflictApp {
// 필드명을 Bean 이름으로 설정
@Autowired
private MyService myServiceImplV2;
...
}
@Qualifier 사용
Bean 등록 시 추가 구분자를 붙여 준다.
생성자 주입, setter 주입 사용 가능
@Component
@Qualifier("firstService")
public class MyServiceImplV1 implements MyService { ... }
@Component
@Qualifier("secondService")
public class MyServiceImplV2 implements MyService { ... }
@Component
public class ConflictApp {
private MyService myService;
// 생성자 주입에 구분자 추가
@Autowired
public ConflictApp(@Qualifier("firstService") MyService myService) {
this.myService = myService;
}
// setter 주입에 구분자 추가
@Autowired
public void setMyService(@Qualifier("firstService") MyService myService) {
this.myService = myService;
}
...
}
@Primary 사용
@Primary로 지정된 Bean이 우선 순위를 가진다.
@Component
public class MyServiceImplV1 implements MyService { ... }
@Component
@Primary
public class MyServiceImplV2 implements MyService { ... }
@Component
public class ConflictApp {
private MyService myService;
@Autowired
public ConflictApp(MyService myService) {
this.myService = myService;
}
...
}
실제 적용 사례
Database가 (메인 MySQL, 보조 Oracle) 두개 존재하는 경우
기본적으로 MySQL을 사용할 때 @Primary를 사용하면 된다.
필요할 때 @Qualifier로 Oracle을 사용하도록 만들 수 있다.
동시에 사용되는 경우 @Qualifier 의 우선순위가 높다.
같은 타입의 Bean이 여러개 조회되었지만 모든 Bean이 필요하다면, Java의 자료구조 Map, List를 사용하는 방법도 있다.
수동 VS 자동
📌 Annotation 기반의 Spring에서는 자동 Bean 등록과 의존관계 주입을 사용하는 경우를 주로 사용한다. @Component 뿐만 아니라 @Controller, @Service, @Repository 등 자동으로 쉽게 등록할 수 있는 Annotation들을 지원하고 Spring Boot는 ComponentScan 방식을 기본으로 사용한다.
📌 Spring으로 구성된 애플리케이션에서 객체(Bean)를 생성, 관리, 소멸하는 역할을 담당한다. 애플리케이션 시작 시, 설정 파일이나 Annotation을 읽어 Bean을 생성하고 주입하는 모든 과정을 컨트롤한다. 심지어는 의존성마저 주입한다.
총괄주방장 = shef 라고 보면 편하다.
Spring Container를 사용하면 인터페이스에만 의존하는 설계가 가능해진다.
OCP, DIP 준수
Spring Container의 종류
BeanFactory
Spring Container의 최상위 인터페이스
Spring Bean을 관리하고 조회한다.
ApplicationContext
BeanFactory의 확장된 형태(implements) -> 진화된 버전
Application 개발에 필요한 다양한 기능을 추가적으로 제공한다.
국제화, 환경변수 분리, 이벤트, 리소스 조회
일반적으로 ApplicationContext를 사용하기 때문에 ApplicationContext를 Spring Container라 표현한다.
Spring Bean
📌 Spring 컨테이너가 관리하는 객체를 의미한다. 자바 객체 자체는 특별하지 않지만, Spring이 이 객체를 관리하는 순간부터 Bean이 된다. Spring은 Bean을 생성, 초기화, 의존성 주입 등을 통해 관리한다.
모든 객체가 Bean인게 아니라 spring container가 관리하는 객체를 bean이라 하는 것
Bean은 new 키워드 대신 사용하는 것이다.
Spring Container가 제어한다.
Spring Bean의 역할
Chef인 Spring Container가 요리할 음식에 사용될 재료(Bean)
요리(Application)의 핵심을 이루는 재료(Bean)
Spring Bean의 특징
Spring 컨테이너에 의해 생성되고 관리된다.
기본적으로 Singleton( 애플리케이션 전역에서 단 하나의 인스턴스만 생성하도록 보장하는 디자인 패턴 )으로 설정된다.
의존성 주입(DI)을 통해 다른 객체들과 의존 관계를 맺을 수 있다.
생성, 초기화, 사용, 소멸의 생명주기를 가진다.
Bean 등록 방법
XML, Java Annotation, Java 설정파일 등을 통해 Bean으로 등록할 수 있다.
XML
<beans>
<!-- myBean이라는 이름의 Bean 정의 -->
<bean id="myBean" class="com.example.MyBean" />
</beans>
--------------------------------------------------------------------
public class MyApp {
public static void main(String[] args) {
// Spring 컨테이너에서 Bean을 가져옴
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyService myService = context.getBean("myService", MyService.class);
myService.doSomething();
}
}
Annotation
@ComponentScan
개발자가 일일이 빈(Bean)을 설정하지 않아도, 지정한 패키지에서 필요한 클래스들을 자동으로 찾아서 애플리케이션 컨텍스트에 빈(Bean) 등록해 줍니다.
// 이 클래스를 Bean으로 등록
// @Controller, @Service, @Repository
@Component
public class MyService {
public void doSomething() {
System.out.println("Spring Bean 으로 동작");
}
}
------------------------------------
@Component
public class MyApp {
private final MyService myService;
@Autowired // 의존성 자동 주입
public MyApp(MyService myService) {
this.myService = myService;
}
public void run() {
myService.doSomething();
}
}
---------------------------------------------
// com.example 패키지를 스캔하여 Bean 등록
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MyApp app = context.getBean(MyApp.class);
app.run();
}
}
Java 설정파일
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyService();
}
}
------------------------------------
public class MyApp {
public static void main(String[] args) {
// Spring 컨테이너에서 Bean을 가져옴
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = context.getBean(MyService.class);
myService.doSomething();
}
}
IOC(제어의 역전, Inversion Of Control)
📌 객체의 생성과 관리 권한을 개발자가 아닌 Spring 컨테이너가 담당하는 것을 말한다. 기본적으로 개발자가 객체를 직접 생성하고 관리했지만, Spring에서는 컨테이너가 객체 생성, 주입, 소멸을 관리한다.
요리사(개발자)는 필요한 재료를 직접 준비하지 않고, Chef가 알아서 필요한 재료(Bean)을 관리하고 요리사에게 가져다준다.
IoC 개념
객체의 생성 및 생명주기 관리를 개발자가 직접 하는 것이 아니라 컨테이너가 담당한다.
객체 간의 결합도를 낮춰 유연한 코드가 된다.
DI(의존성 주입, Dependency Injection)
📌 Spring이 객체 간의 의존성을 자동으로 주입해주는 것을 의미한다. 한 객체가 다른 객체를 사용할 때, 해당 객체를 직접 생성하지 않고 Spring이 주입해주는 방식이다. IOC를 구현하는 방식 중 하나이다.
셰프가 요리를 만들 때 필요한 재료(Bean)를 자동으로 요리사에게 가져다주는 과정
요리사(개발자)는 재료를 찾을 필요 없이, Chef가 알아서 제공해준다.
의존성 = 추상화가 아닌 이유
의존성이란, 한 객체가 다른 객체를 사용하거나 그 객체의 기능에 의존하는 관계를 말합니다. 그리고 추상화는 의존성을 설계하는 중요한 방식 중 하나입니다. 하지만 의존성이 항상 추상화와 동일한 개념은 아닙니다. 의존성과 추상화의 관계를 설명하겠습니다.
의존성은 설계의 결과, 추상화는 설계의 방법:
의존성은 클래스 간의 관계를 나타냅니다.
추상화는 의존성을 관리하거나 줄이기 위한 설계 기법입니다.
구체적인 의존성도 존재:
의존성이 항상 추상화된 타입에만 연결되지 않습니다.
구체 클래스에 대한 의존성도 의존성의 한 형태입니다.
IOC, DI 주입 개발자, SPRING 비교
// Service 인터페이스
public interface MyService {
void doSomething();
}
// Repository 인터페이스
public interface MyRepository {
void queryDatabase();
}
// Service 구현체
public class MyServiceImpl implements MyService {
private MyRepository myRepository;
// 의존성 주입
public MyServiceImpl(MyRepository myRepository) {
this.myRepository = myRepository;
}
@Override
public void doSomething() {
System.out.println("서비스 작업 실행");
myRepository.queryDatabase();
}
}
// Repository 구현체
public class MyRepositoryImpl implements MyRepository {
@Override
public void queryDatabase() {
System.out.println("데이터베이스 쿼리 실행");
}
}
public class MyApp {
public static void main(String[] args) {
MyRepository repo = new MyRepositoryImpl();
// MyRepository repo2 = new MyRepositoryImplV2();
MyService myService = new MyServiceImpl(repo);
// MyService myService2 = new MyServiceImpl(repo2);
myService.doSomething();
}
}
// 새로운 Repository 구현체
public class MyRepositoryImplV2 implements MyRepository {
@Override
public void queryDatabase() {
System.out.println("데이터베이스 쿼리 실행 V2");
}
}
---------------------------------
// Service 구현체
@Service
public class MyIocService implements MyService {
private final MyRepository myRepository;
// 생성자 주입(DI 적용)
@Autowired: 스프링이 자동으로 MyRepository 타입의 빈(Bean)을 찾아 생성자에 주입하도록 지시합니다.
public MyIocService(MyRepository myRepository) {
this.myRepository = myRepository;
}
@Override
public void doSomething() {
System.out.println("IOC 서비스 작업 실행");
myRepository.queryDatabase();
}
}
// Repository 구현체
@Repository
public class MyIocRepository implements MyRepository {
@Override
public void queryDatabase() {
// 데이터베이스와 상호작용
System.out.println("IOC 데이터베이스 쿼리 실행");
}
}
// Spring Container 관리(IoC 적용)
@ComponentScan(basePackages = "com.example")
: @ComponentScan으로 지정된 com.example 패키지를 스캔하여 빈으로 등록할 클래스들을 검색합니다.
public class MyIocApp {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(MyIocApp.class);
// Service 빈을 가져와서 실행
MyService service = context.getBean(MyService.class);
service.doSomething();
}
}
// 새로운 Repository 구현체
@Repository
public class MyIocRepositoryV2 implements MyRepository {
@Override
public void queryDatabase() {
// 데이터베이스와 상호작용
System.out.println("IOC 데이터베이스 쿼리 실행 V2");
}
}
구현 코드가 변경되어도 클라이언트의 코드에는 영향이 없다.
다른 구현체를 구현하여 Bean으로 등록하면 자유롭게 변경이 가능하다.
위 예시 코드는 @Repository 로 등록된 빈이 중복되어 충돌이 발생한다.
의존성 주입(DI), 제어의 역전(IOC)을 통해 객체 간의 결합도를 낮추고 유연한 설계가 가능해진다.
IOC/DI
IoC는 객체의 제어권을 개발자가 아닌 Spring 컨테이너에게 넘기는 개념으로, Spring이 객체 생성과 관리를 담당한다.
스프링에서 Bean을 기본적으로 싱글톤으로 사용하는 이유는 싱글톤 패턴의 장점을 최대한 활용하면서도, 패턴 자체의 단점들을 해결하기 때문입니다. 이를 단계적으로 설명하겠습니다.
싱글톤 패턴의 장점
리소스 절약:
객체를 한 번만 생성하고 재사용하므로 메모리와 리소스를 절약할 수 있습니다.
전역 접근 가능:
애플리케이션 전체에서 동일한 인스턴스를 사용할 수 있어 상태 관리나 공유 자원 관리가 용이합니다.
싱글톤 패턴의 단점과 스프링의 해결책
1. 구현이 복잡하다 (코드가 많다)
문제점:
싱글톤 패턴을 구현하려면 추가적인 코드(정적 변수, 동기화 처리 등)가 필요합니다.
이를 잘못 구현하면 멀티스레드 환경에서 안전하지 않을 수 있습니다.
스프링의 해결:
스프링 컨테이너가 싱글톤 관리를 대신합니다.
개발자는 객체 생성 방식을 신경 쓸 필요 없이, @Component, @Service, @Repository 등의 애너테이션만 추가하면 스프링이 자동으로 관리합니다.
2. 구현 클래스에 의존 (DIP, OCP 위반)
문제점:
싱글톤 패턴은 클래스 자체에서 객체를 관리하기 때문에 구체 클래스에 의존하게 됩니다.
이는 **의존성 역전 원칙(DIP)**과 **개방-폐쇄 원칙(OCP)**을 위반할 가능성을 높입니다.
스프링의 해결:
스프링은 **DI(Dependency Injection)**를 사용하여 의존성을 주입합니다.
Bean을 인터페이스 기반 설계로 사용할 수 있게 하여 DIP를 준수합니다.
개발자는 객체 생성 방식을 몰라도 컨테이너가 관리하는 객체를 사용할 수 있으므로 OCP를 준수합니다.
3. 테스트 어려움
문제점:
싱글톤 객체는 전역 상태를 공유하기 때문에 테스트에서 객체의 상태를 초기화하거나 Mock 객체로 대체하기 어렵습니다.
스프링의 해결:
스프링은 DI를 통해 의존성을 주입하므로, 테스트 환경에서 Mock 객체를 쉽게 주입할 수 있습니다.
빈의 스코프를 싱글톤 이외로 설정(예: 프로토타입 스코프)할 수도 있습니다.
4. 유연성 부족 (안티패턴)
문제점:
전역 상태를 공유하는 싱글톤 객체는 설계의 유연성을 저하시켜 애플리케이션의 확장과 변경에 제약을 줄 수 있습니다.
스프링의 해결:
스프링은 필요에 따라 Bean의 **스코프(scope)**를 변경할 수 있습니다:
싱글톤(Singleton): 기본 설정, 모든 요청에서 동일한 인스턴스를 공유.
프로토타입(Prototype): 요청 시마다 새로운 인스턴스 생성.
요청(Request): HTTP 요청마다 새로운 인스턴스 생성.
세션(Session): HTTP 세션마다 새로운 인스턴스 생성.
웹 소켓(WebSocket): 웹소켓 연결마다 새로운 인스턴스 생성.
스프링이 싱글톤을 사용하는 이유
스프링은 싱글톤 패턴의 단점을 해결하면서도, 다음과 같은 이유로 싱글톤을 기본으로 사용합니다:
효율성:
애플리케이션에서 동일한 빈을 여러 번 생성하지 않고, 한 번 생성된 객체를 재사용하여 리소스를 절약합니다.
글로벌 상태 관리:
데이터베이스 연결, 캐시, 설정 정보 등 전역적으로 공유할 필요가 있는 객체를 관리하기 적합합니다.
유연성:
스프링은 개발자가 직접 싱글톤 패턴을 구현하지 않아도, 컨테이너가 이를 관리합니다.
필요에 따라 스코프를 변경하거나 테스트 시 Mock 객체로 대체할 수 있습니다.
단순화된 코드:
개발자는 @Component, @Service 등 애너테이션만으로 빈을 정의하고 사용할 수 있습니다. 싱글톤 관리 로직을 작성할 필요가 없습니다.
결론
스프링은 싱글톤 패턴의 장점을 최대한 활용하면서, **DI(의존성 주입)**와 유연한 스코프 관리를 통해 단점들을 극복했습니다. 이로 인해 스프링의 Bean은 싱글톤을 기본으로 사용하면서도 효율적이고 확장 가능한 구조를 제공합니다.
Singleton Pattern의 주의
📌 객체의 인스턴스를 하나만 생성하여 공유하는 싱글톤 패턴의 객체는 상태를 유지(stateful)하면 안된다.
지역 변수라면 변수여도 된다. 하지만 요청마다 값이 변경되거나 외부에서 변경 가능한 데이터가 싱글톤 객체에 저장되면 문제가 생길 수 있다.
즉 공유 객체( 여러 사용자나 스레드가 동시에 접근하여 값을 읽거나 수정할 수 있는 데이터 )는 안된다.
상태 유지(stateful)의 문제점
데이터의 불일치나 동시성 문제가 발생할 수 있다.
코드예시
public class StatefulSingleton {
private static StatefulSingleton instance;
// 상태를 나타내는 필드
private int value;
// private 생성자
private StatefulSingleton() {}
// 싱글톤 인스턴스를 반환하는 메서드
public static StatefulSingleton getInstance() {
if (instance == null) {
instance = new StatefulSingleton();
}
return instance;
}
// 상태 변경 메서드
public void setValue(int value) {
this.value = value;
}
// 상태를 반환하는 메서드
public int getValue() {
return this.value;
}
}
public class MainApp {
public static void main(String[] args) {
// 클라이언트 1: 싱글톤 인스턴스를 가져와서 상태를 설정
StatefulSingleton client1 = StatefulSingleton.getInstance();
client1.setValue(42);
System.out.println("클라이언트 1이 설정한 값: " + client1.getValue());
// 클라이언트 2: 동일한 싱글톤 인스턴스를 사용해 상태를 변경
StatefulSingleton client2 = StatefulSingleton.getInstance();
client2.setValue(100);
System.out.println("클라이언트 2가 설정한 값: " + client2.getValue());
// 클라이언트 1이 다시 값을 확인
System.out.println("클라이언트 1이 다시 확인한 값: " + client1.getValue());
}
}
클라이언트 1이 설정한 값: 42 클라이언트 2가 설정한 값: 100 클라이언트 1이 다시 확인한 값: 100