내용: 미니프로젝트를 해보려고 하는데 중요한 부분들 위주로 요약해보려고 한다.
요약 : 애노테이션 정리 여기에 다 했다. 모르는거 있으면 계속 여기로 오자.
- 회원
- 회원을 가입하고 조회할 수 있다.
- 회원은 일반과 VIP 두 가지 등급이 있다.
- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다.
- 주문과 할인 정책
- 회원은 상품을 주문할 수 있다.
- 회원은 등급에 따라 할인 정책을 적용할 수 있다.
- 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라
- 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 있다.
public enum Grade { BASIC, VIP }
enum클래스는 보통 고정된 상수 집합을 나타내는 데 사용됩니다.- 여기서
Gradeenum 클래스는 회원의 등급을 정의하기 위해 사용되고 있습니다.- 이는
BASIC과VIP두 가지 등급만 존재하도록 강제할 수 있습니다.
enum 클래스 사용 이유
- 타입 안전성 제공
enum을 사용하면 특정 값만 가질 수 있습니다. 이는 잘못된 값이 할당되는 것을 방지할 수 있습니다.
- 코드 가독성 향상
enum을 사용하면 코드가 더 읽기 쉬워집니다. 숫자나 문자열 대신 의미 있는 이름으로 등급을 나타낼 수 있습니다.
- 메서드 추가 기능
enum은 단순한 상수 집합 이상의 기능을 가질 수 있습니다. 각 상수에 관련된 메서드를 추가할 수 있습니다.
애플리케이션의 전체 동작 방식을 구성하기 위해 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스를 만든다.
public class AppConfig{ @Bean public MemberService memberService(){ return new MemberServiceImpl(new MemoryMemberRepository()); } public OrderService orderService(){ return new OrderServiceImpl( new MemoryMemberRepository(), new FixDiscountPolicy()); } }
@Bean애노테이션
- 역할 :
@Bean애노테이션은 메서드 수준에서 사용되며, 해당 메서드가 반환되는 객체를 스프링 컨테이너가 관리하는 빈으로 등록합니다.빈(Bean)빈은 스프링 컨테이너가 생성하고 관리하는 객체입니다.
AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성
MemberServiceImplMemoryMemberRepositoryOrderServiceImplFixDiscountPolicy
AppConfig는 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입해준다.
MemberServiceImpl->MemoryMemberRepositoryOrderServiceImpl->MemoryMemberRepository,FixDiscountPolicy
MemberServiceImpl - 생성자 주입
public class MemberServiceImpl implements MemberService{ private final MemberRepository memberRepository; public MemberServiceImpl(MemberRepository memberRepository){ this.memberRepository = memberRepository; } public void join(Member member) { memberRepository.save(member); } public Member findMember(Long memberId) { return memberRepository.findByID(memberId); } }
- 설계 변경으로
MemberServiceImpl은MemoryMemberRepository를 의존하지 않는다.- 단지
MemberRepositoy인터페이스만 의존한다.- 관심사의 분리 : 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.
appConfig객체는memoryMemberRepository객체를 생성하고 그 참조값을memberServiceImpl을 생성하면서 생성자로 전달한다.
클라이언트인memberServiceImpl입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI 의존관계 주입 또는 의존성 주입이라 한다.
- OrderServiceImpl - 생성자 주입
package hello.core.order; import hello.core.discount.DiscountPolicy; import hello.core.member.Member; import hello.core.member.MemberRepository; public class OrderServiceImpl implements OrderService { private final MemberRepository memberRepository; private final DiscountPolicy discountPolicy; public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) { this.memberRepository = memberRepository; this.discountPolicy = discountPolicy; } @Override public Order createOrder(Long memberId, String itemName, int itemPrice) { Member member = memberRepository.findById(memberId); int discountPrice = discountPolicy.discount(member, itemPrice); return new Order(memberId, itemName, itemPrice, discountPrice); } }
- 설계 변경으로
OrderServiceImpl은FixDiscountPolicy를 의존하지 않는다.- 단지
DiscountPolicy인터페이스만 의존한다.OrderServiceImpl입장에서 생성자를 통해 어떤 구현 객체가 들어올지는 알 수 없다.OrderServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig) 에서 결정한다.
AppConfig 실행
public class MemberApp { public static void main(String[] args) { AppConfig appConfig = new AppConfig(); MemberService memberService = appConfig.memberService(); Member member = new Member(1L, "memberA", Grade.VIP); memberService.join(member); Member findMember = memberService.findMember(1L); System.out.println("new member = " + member.getName()); System.out.println("find Member = " + findMember.getName()); } }
OrderApp 실행
public class OrderApp { public static void main(String[] args) { AppConfig appConfig = new AppConfig(); MemberService memberService = appConfig.memberService(); OrderService orderService = appConfig.orderService(); long memberId = 1L; Member member = new Member(memberId, "memberA", Grade.VIP); memberService.join(member); Order order = orderService.createOrder(memberId, "itemA", 10000); System.out.println("order = " + order); } }
public class AppConfig { public MemberService memberService(){ return new MemberServiceImpl(memberRepository()); // return new MemberServiceImpl(new MemoryMemberRepository()); } public OrderService orderService() { return new OrderServiceImpl( memberRepository(), discountPolicy()); } public MemberRepository memberRepository(){ return new MemoryMemberRepository(); } public DiscountPolicy discountPolicy(){ // return new FixDiscountPolicy(); return new RateDiscountPolicy(); } }
new MemoryMemberRepository()이 부분이 중복 제거되었다. 이제MemoryMemberRepository를 다른 구현체로 변경할 때 한 부분만 변경하면 된다.AppConfig를 보면 역할과 구현 클래스가 한눈에 들어온다.
애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.- 만약 할인 정책을 변경한다면, 애플리케이션의 구성 역할을 담당하는 AppConfig 만 변경하면 된다.
AppConfig 스프링 기반으로 변경
@Configuration public class AppConfig { @Bean public MemberService memberService(){ return new MemberServiceImpl(memberRepository()); // return new MemberServiceImpl(new MemoryMemberRepository()); } @Bean public OrderService orderService() { return new OrderServiceImpl( memberRepository(), discountPolicy()); } @Bean public MemberRepository memberRepository(){ return new MemoryMemberRepository(); } @Bean public DiscountPolicy discountPolicy(){ // return new FixDiscountPolicy(); return new RateDiscountPolicy(); } }
- AppConfig에 설정을 구성한다는 뜻의
@Configuration을 붙여준다.
각 메서드에@Bean을 붙여준다. 이렇게 하면 스프링 컨테이너에 스프링 빈으로 등록한다.ApplicationContetxt를 스프링 컨테이너라 한다.- 기존에는 개발자가
AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.- 스프링 컨테이너는
@Configuration이 붙은AppConfig를 설정(구성) 정보로 사용한다. 여기서@Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
요약
@Configuration : 설정 클래스를 정의합니다. 이 클래스는 스프링 컨테이너가 관리하는 빈을 생성하고 구성하는 메서드를 포함합니다.@Bean : 메서드 수준에서 사용되며, 해당 메서드가 반환하는 객체를 스프링 컨테이너에 빈으로 등록합니다.@Configuration 이 붙은 클래스에서 @Bean 메서드를 호출하여 생성된 객체를 관리하고, 필요한 곳에 의존성을 주입합니다.MemberApp에 스프링 컨테이너 적용
public class MemberApp{ public static void main(String[] args) { ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); MemberService memberService = applicationContext.getBean("memberService", MemberService.class); Member member = new Member(1L, "memberA", Grade.VIP); memberservice.join(member); Member findMember = memberService.findMember(1L); System.out.println("new member = " + member.getName()); System.out.println("find Member = " + findMember.getName()); } }
OrderApp도 이와 같이 적용
AppConfig 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.@Configuration 이 붙은 AppConfig 를 설정(구성) 정보로 사용한다. 여기서 @Bean 이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.@Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. ( memberService , orderService )AppConfig 를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다. 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.
- 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다. 한마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다. 개발자 입장에서는 자연스러운 흐름이다.
- 반면에
AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 이제AppConfig가 가져간다. 예를 들어서OrderServiceImpl은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모른다.- 프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다.
- 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라한다.
OrderServiceImpl은DiscountPolicy인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.- 의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다.
- 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.
- 스프링 컨테이너가 생성되는 과정을 알아보자
// 스프링 컨테이너 생성 ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
ApplicationContext를 스프링 컨테이너라 한다.ApplicationContext는 인터페이스이다.- 스프링 컨테이너는 XML을 기반으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스로 만들 수 있다.
- 자바 설정 클래스를 기반으로 스프링 컨테이너(
ApplicationContext) 를 만들어보자.
new AnnotationConfigApplicationContext(AppConfig.class);- 이 클래스는
ApplicationContext인터페이스의 구현체이다.
- 우리가 만들었던 스프링 없는 순수한 DI 컨테이너인
AppConfig는 요청을 할 때마다 객체를 새로 생성한다.- 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다!- > 메모리 낭비가 심하다.
- 해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다 -> 싱글톤 패턴
- 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
- 그래서 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
private생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.싱글톤 패턴 문제점
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
- 의존관계상 클라이언트가 구체 클래스에 의존한다. -> DIP를 위반한다.
- 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
- 테스트하기 어렵다.
- 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다.
- 지금까지의 스프링 빈이 바로 싱글톤으로 관리되는 빈이다.
스프링 컨테이너를 사용하는 테스트 코드
@Test @DisplayName("스프링 컨테이너와 싱글톤") void springContainer() { ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); //1. 조회: 호출할 때 마다 같은 객체를 반환 MemberService memberService1 = ac.getBean("memberService", MemberService.class); //2. 조회: 호출할 때 마다 같은 객체를 반환 MemberService memberService2 = ac.getBean("memberService", MemberService.class); //참조값이 같은 것을 확인 System.out.println("memberService1 = " + memberService1); System.out.println("memberService2 = " + memberService2); //memberService1 == memberService2 assertThat(memberService1).isSameAs(memberService2); }
- 스프링 컨테이너 덕분에 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다.
- 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 고융하기 때문에 싱글톤 객체를 상태를 유지(
stateful)하게 설계하면 안된다.
- 무상태(
stateless)로 설계해야 한다.
- 특정 클라이언트에 의존적인 필드가 있으면 안된다.
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
- 가급적 읽기만 가능해야 한다.
- 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
@Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
memberRepository()처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.
- 크게 고민할 것이 없다. 스프링 설정 정보는 항상
@Configuration을 사용하자.
- 지금까지 스프링 빈을 등록할 때는 자바 코드의
@Bean이나XML의 등을 통해서 설정 정보에 직접 등록할 스프링 빈을 나열했다.- 이렇게 등록해야 할 스프링 빈이 수십, 수백개가 되면 등록하기 귀찮고 누락하는 문제도 발생한다.
- 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
- 또 의존관계도 자동으로 주입하는
@Autowired라는 기능도 제공한다.
컴포넌트 스캔과 자동 의존관계 주입이 어떻게 동작하는지 그림으로 알아보자
1. @Component
@Component은@Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.- 이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.
- 빈 이름 기본 전략:
MemberServiceImpl클래스memberServiceImpl- 빈 이름 직접 지정: 만약 스프링 빈의 이름을 직접 지정하고 싶으면
- @Component("memberService2") 이런식으로 이름을 부여하면 된다.
2. @Autowired 의존관계 자동 주입
- 생성자에
@Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.- 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
getBean(MemberRepository.class)와 동일하다고 이해하면 된다.
MVC 패턴의 등장
비즈니스 로직은 서블릿처럼 다른 곳에서 처리하고, JSP는 목적에 맞게 HTML로 화면을 그리는 일에 집중할 수 있도록 MVC패턴이라는 것이 등장하였다.MVC 패턴(Model View Controller) - 개요
MVC 패턴은 지금까지 학습한 것처럼 하나의 서블릿이나, JSP로 처리하던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 역할을 나눈 것을 말한다. 웹 애플리케이션은 보통 이 MVC 패턴을 사용한다.
- 컨트롤러 : HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 뷰에 전달한 결과 데이터를 조회해서 모델에 담는다.
- 모델 : 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다.
- 뷰 : 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.
MVC 패턴 - 적용
- 서블릿을 컨트롤러로 사용하고, JSP를 뷰로 사용해서 MVC 패턴을 적용할 수 있다.
- Model은
HttpServletRequest객체를 사용한다.request는 내부에 데이터 저장소를 가지고 있는데,request.setAttribute(),request.getAttribute()를 사용하면 데이터를 보관하고, 조회할 수 있다.회원 등록
회원 등록 폼- 컨트롤러@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form") public class MvcMemberFormServlet extends HttpServlet { @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String viewPath = "/WEB-INF/views/new-form.jsp"; RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); dispatcher.forward(request, response); } }
dispatcher.forward(): 다른 서블릿이나 JSP로 이동할 수 있는 기능이다. 서버 내부에서 다시 호출이 발생한다.
redirect vs forward
리다이렉트는 실제 클라이언트에 응답이 나갔다가, 클라이언트가 redirect 경로로 다시 요청한다. 따라서 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경된다. 반면에 포워드는 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 전혀 인지하지 못한다.
회원 저장 컨트롤러와 회원 목록 조회 컨트롤러도 이와 같이 진행된다
MVC패턴 - 한계
MVC 패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링 하는 역할을 명확하게 구분할 수 있다. 특히 뷰는 화면을 그리는 역할에 충실한 덕분에, 코드가 깔끔하고 직관적이다.
하지만, View로 이동하는 코드가 항상 중복되고 사용하지 않는 코드도 존재하며, 공통 저리가 어렵다. 이를 위해 프론트 컨트롤러 패턴을 도입한다.
- 서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입한다. 각 컨트롤러들은 이 인터페이스를 구현하면 된다.프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가져갈 수 있다.
- MyView 라는 클래스를 만들어 단순 반복 되는 뷰 로직을 분리하여 만든다.
- 이전에는 직접 MVC 패턴을 직접 만들어보며 적용해보았다.
- 이제는 애노테이션 기반으로 MVC 패턴을 구현해보자.
- 스프링이 제공하는 컨트롤러는 애노테이션 기반으로 동작해서, 매우 유연하고 실용적이다.
@RequestMapping
RequestMappingHandlerMappingRequestMappingHandlerAdapterRequestMapping에서 찾고Adapter에서 처리
그럼 이제 본격적으로 애노테이션 기반의 컨트롤러를 사용해보자.
1. SpringMemberFormControllerV1 - 회원 등록 폼
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; @Controller public class SpringMemberFormControllerV1 { @RequestMapping("/springmvc/v1/members/new-form") public ModelAndView process() { return new ModelAndView("new-form"); } }
- @Controller:
- 스프링이 자동으로 스프링 빈으로 등록한다. (내부에
@Component애노테이션이 있어서 컴포넌트 스캔의 대상이 된다.)- 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식한다.
- @RequestMapping : 요청 정보를 매핑한다. 해당 URL이 호출되면 이 메서드가 호출된다. 애노테이션을 기반으로 동작하기 때문에, 메서드의 이름은 임의로 지으면 된다.
- ModelandView : 모델과 뷰 정보를 담아서 반환하면 된다.
2. SpringMemberSaveControllerV1 - 회원 저장
@Controller public class SpringMemberSaveControllerV1 { private MemberRepository memberRepository = MemberRepository.getInstance(); @RequestMapping("/springmvc/v1/members/save") public ModelAndView process(HttpServletRequest request, HttpServletResponse response) { String username = request.getParameter("username"); int age = Integer.parseInt(request.getParameter("age")); Member member = new Member(username, age); System.out.println("member = " + member); memberRepository.save(member); ModelAndView mv = new ModelAndView("save-result"); mv.addObject("member", member); return mv; } }
3. SpringMemberListControllerV1 - 회원 목록
@Controller public class SpringMemberListControllerV1 { private MemberRepository memberRepository = MemberRepository.getInstance(); @RequestMapping("/springmvc/v1/members") public ModelAndView process() { List<Member> members = memberRepository.findAll(); ModelAndView mv = new ModelAndView("members"); mv.addObject("members", members); return mv; } }
@RequestMapping 을 잘 보면 클래스 단위가 아니라 메서드 단위에 적용된 것을 확인할 수 있다. 따라서, 컨트롤러 클래스를 유연하게 하나로 통합할 수 있다.
- 실무에서는 지금부터 설명하는 방식을 주로 사용한다. 로직을 전부 통합하여 사용
@Controller @RequestMapping("/springmvc/v3/members") public class SpringMemberControllerV3 { private MemberRepository memberRepository = MemberRepository.getInstance(); @GetMapping("/new-form") public String newForm() { return "new-form"; } @PostMapping("/save") public String save( @RequestParam("username") String username, @RequestParam("age") int age, Model model) { Member member = new Member(username, age); memberRepository.save(member); model.addAttribute("member", member); return "save-result"; } @GetMapping public String members(Model model) { List<Member> members = memberRepository.findAll(); model.addAttribute("members", members); return "members"; } }
- Model 파라미터
save(),members()를 보면 Model을 파라미터로 받는 것을 확인할 수 있다.
- ViewName 직접 반환
- 뷰의 논리 이름을 반환할 수 있다.
- @RequestParam 사용
- 스프링은 HTTP 요청 파라미터를
@RequestParam으로 받을 수 있다.@RequestParam("username")은request.getParameter("username")와 거의 같은 코드라 생각하면 된다. 물론 GET 쿼리 파라미터, POST Form 방식을 모두 지원한다.
@RequestMapping->@GetMapping,@PostMapping
@RequestMapping은 요청 정보를 매핑한다. 해당 URL이 호출되면 이 메서드가 호출된다. 애노테이션을 기반으로 동작하기 때문에, 메서드의 이름은 임의로 지으면 안된다.@RequestMapping은 URL만 매칭하는 것이 아니라,HTTP Method도 함께 구분할 수 있다.- 예를 들어서 URL이 /new-form 이고, HTTP Method가 GET인 경우를 모두 만족하는 매핑을 하려면 다음과 같이 처리하면 된다.
@RequestMapping(value = "/new-form", method = RequestMethod.GET)
이것을@GetMapping,@PostMapping으로 더 편리하게 사용할 수 있다.
참고로 Get, Post, Put, Delete, Patch 모두 애노테이션이 준비되어 있다.
Component와 Controller는 뭐가 달라요?
- 용도와 역할:
@Component: 스프링 컴포넌트 스캔을 통해 해당 클래스를 스프링 빈으로 등록.@Controller: Spring MVC에서 HTTP 요청을 처리하고 응답을 반환하는 컨트롤러 역할.
@Component로@Controller를 대체 가능한가?
- 불가능합니다.
@Controller는@Component를 포함하며, 추가적인 요청 처리 기능을 제공합니다.
- Controller 클래스의 역할:
- HTTP 요청과 응답 처리, URL 매핑, 서비스와 뷰 연결 등 클라이언트와 애플리케이션의 중간다리 역할.
@Controller의 정체:
@Controller는@Component를 메타 어노테이션으로 포함하며, 요청 매핑 및 Spring MVC 컨트롤러 역할을 수행하도록 설계.
- 결론:
@Component는 단순히 빈을 등록하지만,@Controller는 요청과 응답 처리를 위한 기능을 제공합니다. 따라서@Controller는 단순한@Component보다 더 많은 기능을 내포합니다.
로깅 간단히 알아보기
앞으로 로그를 사용할 것이기 때문에, 이번시간에는 로그에 대해서 간단히 알아보자.
스프링 부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리(
spring-boot-starter-logging)가 함께 포함된다.
스프링 부트 로깅 라이브러리는 기본으로 다음 로깅 라이브러리를 사용한다.
SLF4J - http://www.slf4j.org
Logback - http://logback.qos.ch
로그 선언
private Logger log = LoggerFactory.getLogger(getClass());private static final Logger log = LoggerFactory.getLogger(Xxx.class)@Slf4j: 롬복 사용 가능
getClass()는 현재 내 클래스의 로그를 출력한다는 의미
로그 호출
log.info("hello")
System.out.println("hello")
LogTestController
//@Slf4j @RestController public class LogTestController { private final Logger log = LoggerFactory.getLogger(getClass()); @RequestMapping("/log-test") public String logTest() { String name = "Spring"; log.trace("trace log={}", name); log.debug("debug log={}", name); log.info(" info log={}", name); log.warn(" warn log={}", name); log.error("error log={}", name); //로그를 사용하지 않아도 a+b 계산 로직이 먼저 실행,이런 방식 사용하면 X log.debug("String concat log=" + name); return "ok"; } }
매핑 정보
@RestController
@Controller는 반환 값이String이면 뷰 이름으로 인식된다. 그래서 뷰를 찾고 뷰가 랜더링 된다.@RestController는 반환 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력한다. 따라서, 실행 결과로 ok 메시지를 받을 수 있다.테스트
- 로그가 출력되는 포멧 확인
- 시간, 로그 레벨, 프로세스 ID, 쓰레드 명, 클래스명, 로그 메시지
- 로그 레벨 설정을 변경해서 출력 결과를 보자.
- LEVEL: TRACE > DEBUG > INFO > WARN > ERROR
- 개발 서버는 debug 출력
- 운영 서버는 info 출력
@Slf4j로 변경
로그 레벨 설정(application.properties)
#전체 로그 레벨 설정(기본 info)
logging.level.root=info#hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug
/** * PathVariable 사용 * 변수명이 같으면 생략 가능 * @PathVariable("userId") String userId -> @PathVariable userId */ @GetMapping("/mapping/{userId}") public String mappingPath(@PathVariable("userId") String data) { log.info("mappingPath userId={}", data); return "ok"; }
최근 HTTP API는 다음과 같이 리소스 경로에 식별자를 넣는 스타일을 선호한다.
/mapping/userA
/users/1
@RequestMapping은 URI 경로를 템플릿화 할 수 있는데,@PathVariable을 사용하면 매칭 되는 부분을 편리하게 조회할 수 있다.
/** * PathVariable 사용 다중 */ @GetMapping("/mapping/users/{userId}/orders/{orderId}") public String mappingPath(@PathVariable String userId, @PathVariable Long orderId) { log.info("mappingPath userId={}, orderId={}", userId, orderId); return "ok"; }
/** * Content-Type 헤더 기반 추가 매핑 Media Type * consumes="application/json" * consumes="!application/json" * consumes="application/*" * consumes="*\/*" * MediaType.APPLICATION_JSON_VALUE */ @PostMapping(value = "/mapping-consume", consumes = "application/json") public String mappingConsumes() { log.info("mappingConsumes"); return "ok"; }
1. HTTP 요청 데이터 조회 - 개요
HTTP 요청을 통해 클라이언트에서 서버로 데이터를 전달하는 방법을 알아보자.
- GET - 쿼리 파라미터
- /url?username=hello&age=20
- 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
- 예) 검색, 필터, 페이징등에서 많이 사용하는 방식
- POST - HTML Form
- content-type: application/x-www-form-urlencoded
- 메시지 바디에 쿼리 파라미터 형식으로 전달 username=hello&age = 20
- 예) 회원 가입, 상품 주문, HTML Form 사용
- HTTP message body 에 데이터를 직접 담아서 요청
- HTTP API에서 주로 사용, JSON, XML, TEXT
- 데이터 형식은 주로 JSON 사용
- POST, PUT, PATCH
2. 요청 파라미터 - 쿼리 파라미터, HTML Form
HttpServletRequest의requestgetParameter()를 사용하면 다음 두가지 요청 파라미터를 조회할 수 있다.GET, 쿼리 파라미터 전송
예시 : `http://localhost:8080/request-param?username=hello&age=20`POST, HTML Form 전송
예시POST /request-param ... content-type: application/x-www-form-urlencoded username=hello&age=20GET 쿼리 파라미터 전송 방식이든, POST HTML Form 전송 방식이든 둘다 형식이 같으므로 구분없이 조회할 수 있다. 이것을 간단히 요청 파라미터(
request parameter) 조회라 한다.
2-1. HTTP 요청 파라미터 - @RequestParam
스프링이 제공하는
@RequestParam을 사용하면 요청 파라미터를 매우 편리하게 사용할 수 있다.
requestParamV2
/** * @RequestParam 사용 * - 파라미터 이름으로 바인딩 * @ResponseBody 추가 * - View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력 */ @ResponseBody @RequestMapping("/request-param-v2") public String requestParamV2( @RequestParam("username") String memberName, @RequestParam("age") int memberAge) { log.info("username={}, age={}", memberName, memberAge); return "ok"; }
@RequestParam: 파라미터 이름으로 바인딩@ResponseBody: View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력
requestParamV3
/** * @RequestParam 사용 * HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능 */ @ResponseBody @RequestMapping("/request-param-v3") public String requestParamV3( @RequestParam String username, @RequestParam int age) { log.info("username={}, age={}", username, age); return "ok"; }
requestParamV4
/** * @RequestParam 사용 * String, int 등의 단순 타입이면 @RequestParam 도 생략 가능 */ @ResponseBody @RequestMapping("/request-param-v4") public String requestParamV4(String username, int age) { log.info("username={}, age={}", username, age); return "ok"; }
2-2. HTTP 요청 파라미터 - @ModelAttribute(중요!)
실제 개발을 하면 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야 한다. 보통 다음과 같이 코드를 작성한다.
@RequestParam String username; @RequestParam int age; HelloData data = new HelloData(); data.setUsername(username); data.setAge(age);
- 스프링은 이 과정을 완전히 자동화해주는
@ModelAttribute기능을 제공한다.
먼저 요청 파라미터를 바인딩 받을 객체를 만들자
import lombok.Data; @Data public class HelloData { private String username; private int age; }
@ResponseBody @RequestMapping("/model-attribute-v1") public String modelAttributeV1(@ModelAttribute HelloData helloData) { log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); return "ok"; }
HelloData객체가 생성되고, 요청 파라미터의 값도 모두 들어가 있다.- 스프링 MVC는
@ModelAttribute가 있으면 다음을 실행한다.
- HelloData 객체를 생성한다.
- 요청 파라미터의 이름으로
HelloData객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력한다.- 예) 파라미터의 이름이 username 이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력한다.
3. HTTP 요청 메시지 -단순 텍스트
- HTTP message body에 데이터를 직접 담아서 요청
- HTTP API에서 주로 사용, JSON, XML, TEXT
- 데이터 형식은 주로 JSON 사용
- POST, PUT, PATCH
- 요청 파라미터와 다르게, HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우
@RequestParam,@ModelAttribute를 사용할 수 없다.@RequestBody를 사용하자
@RequestBody - requestBodyStringV4
@RequestBody를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회할 수 있다. 참고로 헤더 정보가 필요하다면HttpEntity를 사용하거나@RequestHeader를 사용하면 된다.- 이렇게 메시지 바디를 직접 조회하는 기능은 요청 파라미터를 조회하는
@RequestParam,@ModelAttribute와는 전혀 관계가 없다.@ResponseBody @PostMapping("/request-body-string-v4") public String requestBodyStringV4(@RequestBody String messageBody) { log.info("messageBody={}", messageBody); return "ok"; }
- 정리 :
@RequestBody를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회할 수 있다
HTTP 요청 메시지 - JSON
HTTP API에서 주로 사용하는 JSON 데이터 형식을 조회해보자.
이때도, 결국에는@RequestBody를 사용해주면 된다.requestBodyJsonV5
@ResponseBody @PostMapping("/request-body-json-v5") public HelloData requestBodyJsonV5(@RequestBody HelloData data){ log.info("username={}, age={}", data.getUsername(), data.getAge()); return data; } }
@ResponseBody
응답의 경우에도@ResponseBody를 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있다. 물론 이 경우에도HttpEntity를 사용해도 된다.
@RequestBody요청
- JSON 요청 -> HTTP 메시지 컨버터 -> 객체
@ResponseBody응답
- 객체 -> HTTP 메시지 컨버터 -> JSON 응답
총정리
@RequestParam와@ModelAttribute은 HTTP 요청의 파라미터를 처리합니다.
- 와
@RequestParam은 HTML 폼 데이터나 쿼리 파라미터를 처리할 때 사용됩니다.
@RequestBody와@ResponseBody는 HTTP 요청과 응답의 본문(body)을 처리합니다.
@RequestBody와@ResponseBody는 주로 JSON 또는 XML과 같은 데이터를 주고받을 때 사용됩니다.
@Controllervs@ResponseBody
@Controller가 붙은 클래스의 메서드는 기본적으로 뷰(View)를 반환합니다. 즉, JSP, Thymeleaf 같은 템플릿 엔진을 통해 HTML 페이지를 생성하고, 이를 클라이언트에 전달합니다.- 이 애노테이션을 사용하면 메서드의 반환값이 뷰 리졸버(
View Resolver)를 거치지 않고, 그대로 HTTP 응답으로 전달됩니다.