지난 포스팅에서 6장까지 알아보았는데, 6장까지는 핵심 원리에 가까운 내용이었다.
이번 포스팅에서는 7장부터 끝까지, 스프링의 핵심 기능(물론 원리도 포함)에 대해 알아보자.
저번 포스팅을 보면서 "내가 생각한 핵심 원리"이라는 주제로 글을 포스팅한 것은 좋았는데,
막상 그걸 보았을 때 "어떤 도움"이 되는지 확실하게 생각해보지 못했다.
Q: 그럼 쓸데없는 내용을 빼고 혼자 내용을 간추렸다는 건가요? 김영한님 강의를? 무슨 기준으로? 무슨 역량으로..?
A: 물론 혼자 간추리면 뭐가 틀려도 단단히 틀릴 수 있겠지만,
그 내용을 여기 포스팅에 여실히 기록하기로 했다.
내가 정리한게 다 맞을 필요도 없고(내가 복습할 내용인데 뭐) 무엇보다,
그래야 내가 공부했을 때 무엇을 잘못 이해했는지, 놓친 것은 없었는지, 또 처음 수강할때와 2,3회차 수강할때 새롭게 얻은 지식이 무엇인지 확실히 알 수 있을 것 같아서 그렇다. 특히 이번 시리즈는 스프링 강의를 달리는 동안, "왜?" 라는 키워드에 초점을 맞추려한다.
그래야 내가 스프링을 잘 사용할 수 있을 것 같다.
프로젝트를 하면서 스프링의 여러 기능을 사용해봤지만, 어떻게 쓰는지만 알지 '왜 써야하는지' 에 대한 고민을 많이하지 못했다.
스프링이라는 무기에 익숙해지기 위해 좀 더 고민을 하기로 했다. 물론 그렇게 되기 위해 기본 내용 숙지는 필수!!
이번 포스팅은 내용이 많다.
지난 시간에 싱글톤의 필요성은 한번 보았었다.
왜 필요한데?

GC파티가 일어날것
싱글톤 빈에 대한 내용이 좀 많은데, 일단 이 내용을 살펴보기 전에 컴포넌트 스캔이란걸 한번 보자.
component 스캔
스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔을 제공
-> 왜요? @Bean으로 스프링 빈을 하나하나(수백개를) 등록하기 귀찮아서요. 간혹 누락되기도 하고
그래서 컴포넌트 스캔을 이용하면
@Bean 어노테이션 없이도 이 기능을 사용해 @configuration이 붙은 설정 정보가 자동 등록된다.
그치만
@Bean으로 직접 설정정보를 작성하고 의존관계를 명시했는데,
@Component를 이용해 스캔하면 의존관계가 없는거 아닌가요?
그래서 의존관계도 자동으로 주입하는 @Autowired도 스프링은 제공한다.
과정
- 컴포넌트 스캔: @Component 어노테이션이 붙은 모든 클래스를 스프링 빈으로 등록, 이때 클래스명을 쓰고 첫글자는 소문자로 쓴다. (직접 지정도 가능)
- 생성자에 @Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다. 조회 전략은 “타입이 같은 빈”을 찾아서 주입한다.
아 근데 귀찮다면서 이거 쓰면 얼마나 편하지?
기본적으로 스프링 부트에서 빈 등록을 신경쓰지 않아도 될...정도
다만 계층별 어노테이션에 @Component가 붙어있다는 걸 알아두자.
또한 @Repository같은 경우 해당 클래스를 스프링 데이터접근계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
필터
필터란 놈도 있다. 얘는 강사님 피셜로 그렇게 많이 안 쓴다고 하셨으니 간단하게 설명하면
"스캔에서 제외할 대상"이다.
음.. 명료하군
끝. (이러다가 나중에 요거 때문에 다시 뒷통수 맞는 날이 오면 수정하러 오겠음)
중복 등록과 충돌
-> 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같으면 스프링이 오류를 발생시킨다.
`ConflictingBeanDefinitionException`란다. 얘는 자동 빈 등록끼리에서 발생한다. 오류는 발생할 수 있지 음음.
근데 수동 빈과 자동 빈이 충돌하면?
수동 빈이 우선권을 가진다 -> 오버라이딩해버린다 -> 오류 x..?
=> 에러도 없이 개발자의 의도와 다르게 작동한다 => 원인을 찾기 힘들다 => 해결에 오래 걸린다.
난 2회차 수강을 하며 이런걸 잘 배워놓기를 바랬는데, 이해가 되어서 다행이다....
암튼 최신 스프링 부트는, 이런 충돌시 오류가 발생하게 기본값을 바꾸었다고 한다.
항상 느끼는 거지만, 너무 빠르게 새 버전이 나오다보니, 이전 문서가 현재 호환되지 않는 경우도 많다.
(특히 처음 시큐리티할때 스프링부트2.x 버전에 무슨 deperated된 코드들... 안 그래도 스프링 처음 공부하는데 죽고싶었다)
위 경우는 최신 버전에서는 신경안써도 되도록 바뀌었지만, 그렇다고 아예 그 속 내용 공부를 안해버리면 또 빈 껍데기만 훑는 결과가 될수도 있겠다 하는 생각이 든다. 항상 이론과 실전에서 갈등하게 된다...
자 대음.
왜 생성자 주입인가?
의존관계 주입 → 웬만하면 생성자 주입으로만 하자
너무 중요한 내용이지만 오히려 간략하게 정리하기
생성자 주입
특징: 딱 1번만 호출하는게 보장
이것의 장점? 불변, 필수 의존관계에 사용한다.
생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입된다.
주입 시점
생성자 주입은 빈을 등록하면서 의존관계도 같이 주입이 들어감 (뒤에 더 자세한 내용이 나온다)
수정자 주입: 필드의 값을 변경할때 관례상 (set+필드명)을 통해 수정하는데 이를 수정자라고하는데, 여기에 @Autowired를 넣는 것
주입시점
수정자 주입은 단계가 나뉘어 각각 주입이 된다.
특징: 선택적으로 의존관계를 주입
@Autowired(required= false)로 선택 가능
필드 주입 → 그냥 안쓰는게 맞음
왜요?
DI 프레임워크가 없으면 아무것도 할 수 없기 때문
(외부에서 변경이 불가능함)
그래서 생성자 주입이 좋은 이유는?
1. 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다.
오히려 불변해야한다.
누군가 실수로 변경하면 안되지! → 불변하게 설계하기
2. 누락(수정자 주입의 문제)
프레임워크 없는 순수한 자바 코드를 단위 테스트하는 경우
수정자 의존관계의 경우 NPE 예외 발생 ->(의존관계 주입 누락)
테스트를 돌리는 개발자 시점 -> 의존관계가 눈에 안 보임 → 코드를 까봐야 앎
이걸 생성자로 지정을 하면 필수값을 알려주기 때문에 컴파일 오류
→ IDE에서 어떤 값을 필수로 주입해야하는지 알수 있음
3. final 키워드
생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있음.
그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에서 막아줌
조회할 빈이 2개이상인 경우
@Autowired 는 타입으로 빈을 찾는데, ac.getBean(DiscountPolicy.class)와 유사하다.
(DiscountPolicy는 강의의 예제 인터페이스)
DiscountPolicy의 하위 타입인 FixDiscountPolicy,RateDiscountPolicy가 있다고 하자.
이걸 둘다 스프링 빈으로 선언하고 DiscountPolicy에 의존관계 자동 주입을 실행하면
NoUniqueBeanDefinationException이 발생
그렇다고 요걸 하위타입으로 고정해버리면 DIP가 지켜지지 않아서 해결이 안됨
빈 수동 등록을 통해 해결할 수도 있지만, 다른 방법이 있다.
@Autowired 필드명,@Qualifier, @Primary
이걸 알아보기전에 이런 상황이 실제로 많이 일어나나? 궁금증이 생겼다.
난 실무라고 할만한 큰 프로젝트를 한적은 없어서, 이런 상황이 자주 일어나는지 검색해보았다.

음 역시 중요한 내용이다. 더 자세한 예시를 알 수 있을까?
예를 들어 결제 서비스에서 아래와 같은 코드라면
// 결제 처리를 위한a 인터페이스
public interface PaymentService {
void processPayment(Order order);
}
// 신용카드 결제 구현체
@Service
public class CreditCardPaymentService implements PaymentService {
@Override
public void processPayment(Order order) {
// 신용카드 결제 처리 로직
}
}
// 계좌이체 결제 구현체
@Service
public class BankTransferPaymentService implements PaymentService {
@Override
public void processPayment(Order order) {
// 계좌이체 결제 처리 로직
}
}
// 주문 컨트롤러
@RestController
public class OrderController {
// 문제 발생! 어떤 PaymentService를 주입해야 할지 Spring이 결정할 수 없음
@Autowired
private PaymentService paymentService;
// ...
}
//문제발생 ㅋㅋㅋ
암튼 이런 문제가 발생할 수 있다.
그럼 해결방법은??
우선 필드명 매칭이 있다.
@Autowired에서 필드명을 딱 찍어서
DiscountPolicy discountPolicy대신
DiscountPolicy rateDiscountPolicy 로 해주는것
….
이거 그냥 IDE에서 구분하기 편하라고 자동완성해주는건줄 알고오 개꿀하면서 썼던건데… 필드명 매칭이어서 에러가 안났던거였다…
@Qualifier 사용 추가 구분자를 붙여주는 방식이다. 구분 옵션이 하나 더 생긴다고 생각하면 편하다.
@Qualifier(”mainDiscountPolicy”) 를 하위 타입의 클래스 어노테이션으로 붙이기 + 상위 타입의 생성자 파라미터의 타입 앞에 어노테이션으로 똑같이 명시하기로 구현할 수 있다.
@Primary 우선 순위를 정하는 방법이다. 그냥 하위 타입 클래스에 이걸 지정하면 그 클래스가 우선순위가 최상위로 올라간다. 생각보다 자주 쓴다. (지저분하지 않아서 ㅋㅋ)
그래서 뭘 쓸까?
메인 데이터베이스와 서브 데이터베이스의 커넥션을 획득하는 빈이 있다고 생각하면,
메인에서는 @Primary로 간편하게 사용할 수 있고, 서브에서는 @Qualifier로 상세하게 로직을 다룰 수 있다. 그리고 자세한 것이 항상 우선순위가 높으니 @Qualifier가 가장 우선권이 높다는 걸 기억하자.
또 어노테이션을 직접 만들 수도 있다. (과정은 생략)
사용하는 이유는 바로 컴파일 오류를 위해서
@Qualifier에서 이름을 명시한다면, 문자이기 때문에 타입체크가 안되어 컴파일 시점에 오류를 잡기 어렵다.
따라서 하나의 어노테이션으로 만들면 오탈자가 바로 에러를 발생시키므로 문제해결이 가능하다.
조회할 빈이 모두 필요할 때, List, Map
이 부분은 내가 공부를 덜한 부분이라 따로 코드를 정리했다.
public class AllBeanTest {
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(
AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "userA", Grade.VIP);
int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
assertThat(discountService).isInstanceOf(DiscountService.class);
assertThat(discountPrice).isEqualTo(1000);
}
static class DiscountService {
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
System.out.println("discountCode = " + discountCode);
System.out.println("discountPolicy = " + discountPolicy);
return discountPolicy.discount(member, price);
}
}
}
로직 분석
DiscountService는 Map으로 모든 DiscountPolicy 를 주입받는다. 이때 fixDiscountPolicy ,rateDiscountPolicy가 주입된다.
discount () 메서드는 discountCode로 fixDiscountPolicy가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행한다.
물론 rateDiscountPolicy가 넘어오면 rateDiscountPolicy 스프링 빈을 찾아서 실행한다.
주입분석
Map<String,DiscountPolicy>: map의 키에 빈의 이름을 넣고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
List<DiscountPolicy>: DiscountPolicy타입으로 조회한 모든 스프링 빈을 담아준다.
자동, 수동의 올바른 실무 운영기준
드디어 내가 알고 싶은 내용!
편리한 자동기능을 기본으로 사용
스프링부트가 계속해서 자동으로 진화한다. 너무 편리하고 정확하다. 그냥 @Component만 넣어주면 되는데
그럼 언제 수동 빈등록을??
어플은 크게 업무로직, 기술 지원로직으로 나뉜다.
- 업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리등이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
- 기술 지원 빈: 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
어플에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 설정 정보에 바로 나타나게 하는 것이 유지보수하기 좋다!!!
또한 비즈니스 로직 중에서 다형성을 적극 활용할 때, 이 경우 수동으로 등록하거나, 자동으로 하면 특정 패키지에 같이 묶어두는게 좋다!!
(빈이 여러개인데 내가 혼자만 알아보게 우선순위를 정해놨다면 헷갈릴 것)
꼭 나중에 써먹어 봐야지
빈 생명주기 콜백
왜 배울까?
여기는 처음 강의를 들었을때는 이해하지 못했던 부분이다.

스프링 빈은 간단하게 아래와 같은 라이프사이클을 가진다.
객체 생성 → 의존관계 주입
(생성자 주입은 객체 생성시 같이 스프링 빈이 들어와야 하므로 예외)
빈은 객체를 생성하고 의존관계 주입이 끝나야 준비가 끝남.
초기화 작업은, 의존 관계 주입이 끝나고 호출해야 함.
그럼 개발자는 의존관계 주입이 끝난 시점을 어떻게 알까?
스프링은 의존관계 주입이 끝나면 콜백 메서드를 통해 초기화 시점을 알려주는 기능이 많이 있음. 또한 스프링 컨테이너가 종료되기 전 소멸 콜백도 줌.
⇒ 따라서 안전하게 종료를 할 수 있음
컨테이너 생성 - 빈 생성 - 의존관계 주입 - 초기화 콜백 - 사용 - 소멸 콜백 - 빈 종료
자 그럼 생기는 의문
Q: 그냥 생성자에서 파라미터랑 의존관계 주입 다 주면 되지 않나?
A1: SRP 위배 -> 생성자에서는 객체 생성에 책임만 지도록 해야함
A2: 객체의 실제 동작하거나 하는 행위는 별도의 행위로 분리하는게 좋음. 메모리 관점 혹은 유지보수 관점에서..
초기화는 생성된 값을 활용해 외부 커넥션을 연결하는 등 무거운 동작 수행
그래서 전지전능하신 스프링은 뭘로 생명주기 콜백을 지원하나??
총 3가지 방법이 있는데, 하나만 정리한다.
어노테이션 @PostConstruct, @PreDestroy
→ 이걸 써라.
매우 편리 → 자바 표준이라 스프링이 아닌 다른 컨테이너에서도 동작함
+컴포넌트 스캔과 잘 어울림 but 외부 라이브러리에 사용불가
→ 외부 라이브러리(코드 수정 불가)에는 @Bean의 initmethod, destroyMethod를 사용하자.
빈 스코프
으아 이제 마지막이다.
왜 배울까?
(참고로 여기서 강의에 불만이 좀 있었는데, 배우는 이유를 너무 늦게 알려주셔서 그랬다..그냥 그랬다구요..)
나도 늦게 적어놔야지 ㅋ
스프링 빈이 컨테이너가 만들어진 뒤 빈들도 함께 생성,관리,종료가 되는데 기본적으로 싱글톤 스코프로 생성되기 때문임.
프로토타입 스코프를 만들면, 빈을 만들고 의존관계 주입까지만 한 뒤에 관리를 안하고 방치하는 것
결국 요청마다 새로운 빈을 만들어 반환하는 것이다.
처음 GC를 줄이기위해 싱글톤 빈이 생긴 이유와 반대
핵심은 스프링 컨테이너는 의존관계 주입, 초기화까지만 처리한다는 것
그럼 만약 종료메서드를 호출해줘야 한다면,
클라이언트가 책임지고 종료메서드를 호출해줘야 함
그래서 @PreDestroy같은 종료메서드가 호출되지 않는다!!
(그래서 왜 배우는데요)
강의는 계속되었다.
싱글톤 빈과 프로토타입 빈을 함께 쓸 때 문제점.
그림으로 이해해도 되지만, 결국 싱글톤 빈에 프로토타입 빈이 의존관계로 들어간다면, 얘는 이제 싱글톤처럼 관리가 되어버리는게 문제다.
다시 말하면 제 기능을 못한다는 소리.
좋은 해결 방법은 Provider라는 것을 쓰는 것이다.
의존관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존관계 조회(탐색) 이라한다.
ObjectFactory, ObjectProvider 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider 이다.
Provider를 사용하면 ObjectProvider의 getObject() 메서드가 호출되는 시점까지 빈의 조회를 지연한다.
그치만 스프링에 의존적임
JSR-330 Provider를 쓰는 방법도 잇음
다만 gradle에 의존성 추가가 필요
get하나로 기능이 단순. 자바 표준
그리고
하…. 드디어 생명주기를 왜 배우는지 나왔다.
근데 그냥 정의 그대로다.
"매법 사용할 때마다 의존관계 주입이 필요한 새로운 객체가 필요한 경우"
그래서 그게 어떨 땐데요ㅠㅠㅠ
안되겠다 찾아보자.

무거운 계산 작업을 수행하는 객체
무거운 계산 작업을 수행하는 객체는 대량의 메모리나 CPU 자원을 사용하는 작업을 처리하는 경우에 프로토타입 빈으로 선언하면 유용합니다.
이유는?
- 자원 관리: 대용량 메모리를 사용하는 작업이 필요할 때만 인스턴스를 생성하고, 작업 완료 후 GC가 메모리를 회수할 수 있게 합니다.
- 작업 격리: 여러 분석 작업이 동시에 실행될 때 서로 간섭하지 않고 독립적으로 실행됩니다.
- 파라미터 독립성: 각 분석 작업마다 다른 파라미터와 데이터셋을 사용할 수 있습니다.
만약 무거운 계산 작업을 수행하는 객체를 싱글톤으로 관리한다면:
- 모든 요청이 동일한 인스턴스를 공유하게 되어, 한 요청이 대용량 메모리를 할당하면 다른 요청들도 영향을 받음.
- 작업이 끝나도 인스턴스가 계속 메모리에 남아있어 불필요한 자원을 점유.
반면 프로토타입 스코프로 선언하면 작업이 완료된 후 참조가 사라지면 GC가 해당 객체를 수거할 수 있어 메모리 관리가 효율적
오 그럴듯해
멀티스레드 환경에서의 날짜 포맷터
이유는?
SimpleDateFormat은 스레드 안전하지 않은 대표적인 클래스
날짜 패턴을 파싱하고 포맷팅하는 내부 상태를 가지고 있어, 여러 스레드에서 동시에 접근하면 예상치 못한 결과나 예외가 발생할 수 있다.
- 스레드 안전성: 각 스레드가 자신만의 포맷터 인스턴스를 사용하므로 동시성 문제가 발생하지 않음
- 패턴 독립성: 다양한 형식의 날짜 포맷팅이 필요할 때 각 인스턴스가 자신만의 패턴을 가질 수 있음.
- 로케일과 타임존 독립성: 글로벌 애플리케이션에서 사용자별로 다른 로케일과 타임존을 지원할 수 있음.
하 근데 이건 잘 모르겠다.... 나중에 멀티 쓰레드를 더 공부하고 다시 정리해야겠다.
웹 스코프
싱글톤은 시작과 끝
프로토타입은 생성,의존관계 주입,초기화시 끝남
웹 스코프는
웹 환경에서만 동작, 스프링이 종료시점까지 관리해줌
문제발생!!
생존 범위는 고객 요청이 들어와서 나갈때까지인데
그럼 처음 서버를 띄울때부터 고객 요청이 들어오기 전에, 스프링 컨테이너가 주입해줘야 하는데 request 스코프가 활성화되지 않는 문제가 생김. 생명 주기가 다르다!
그래서 아까 배운 provider로 해결이 가능한데, 더 간단하게 쓰고 싶은 욕심이 생김
→ Proxy
핵심: @Scope에 value= “request”, proxyMode=ScopedProxyMode.TARGET_CLASS 추가
(적용 대상이 인터페이스가 아닌 클래스면 TARGET_CLASS를 선택
적용대상이 인터페이스면 INTERFACES를 선택)
이전에 "생명 주기가 달라서 주입받을 시점에 request 스코프가 없어 났던 에러"가 이제 사라진다.
이것의 원리는?
싱글톤 보장이라는 개념에서 봤던 CGLIB
가짜 프록시(껍데기) 객체를 먼저 주입하고, 나중에 요청이 들어오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
이 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 잇다.
ex) 전체 요청당 몇초, 각 메서드 호출에 걸린 시간을 다 찍는다 → Spring AOP와 proxy를 이용해서 쉽게 할 수 있음
핵심 아이디어는, 진짜 객체 조회를 꼭 필요한 시점까지 지연처리하는 것
→ 어노테이션 설정 변경만으로 원래 객체를 프록시 객체로 대체 = 바로 다형성과 DI의 힘
이게 제일 중요한 내용인거 같다.
이런 scope는 정말 특별할때만 사용한단다... 다 배워놓고 ㅋㅋㅋ ㅠㅠㅠㅠ
이렇게 순식간에 두번의 포스팅(사실상 포스팅 하나)으로 스프링 핵심 개념/기본 편 정리가 끝났다.
생략된 부분이 많지만 스스로 느끼기에 중요하다고 생각하는 내용은 모두 정리했다. 앞으로 기본 개념이 헷갈릴때면 이 포스팅을 찾아와서 복습하고, 틀린 내용이 있다면 고쳐나가겠다.
내가 강의를 보면서 느낀 실무에서 선택하는 기술의 기준은 크게 세가지 인것 같다.
1. 안 복잡하고 안 어려운가
2. 객체 지향 원리를 위배하지 않는가
3. 자바 표준인가 혹은 스프링에 종속적인가(이건 좋고 나쁘고 보다는? 선택인 것 같다. 상황에 맞게!)
많은 기능을 배웠다. 특히 프록시는 한번 사용해보고 싶다.
하지만 써야 할 필요가 있을 때 기술을 도입해야 그 의미가 나타나기에, 다음 프로젝트에서 기회가 되면 시도해 보겠다.
다음은 스프링 MVC 편인데, 내용이 얼마나 될지는 모르겠지만 이번 포스팅 보다는 줄여서...ㅋㅋㅋ 올려보도록 하겠다.
끝!
'Spring_Why?' 카테고리의 다른 글
| [Spring/MVC] 스프링 웹 MVC 마무리 (1) | 2025.05.02 |
|---|---|
| [Spring/MVC] 직접 만드는 MVC 패턴 - FrontController 도입 (1) | 2025.04.24 |
| [Spring/MVC] 스프링 MVC 이전의 개발 (0) | 2025.04.16 |
| [Spring/기본] 스프링, 내가 생각한 핵심 개념 (0) | 2025.04.07 |