스프링을 사용한 개발에 기본적인 흐름을 배워보자
MVC, 템플릿 엔진
- MVC : Model, View, Controller
@Controller
public class HelloController {
@GetMapping("hello-mvc")
public String helloMvc(@RequestParam("name") String name, Model model) {
model.addAttribute("name", name);
return "hello-template";
}
스프링 MVC와 타임리프 템플릿 엔진을 사용한 코드이다.
1. @Controller
- 이 클래스가 spring MVC의 컨트롤러임을 나타내는 어노테이션, 브라우저의 요청을 처리하고 뷰(HTML)와 데이터를 연결하는 역할
2. @GetMapping("hello-mvc")
- /hello-mvc 경로로 들어오는 GET 요청을 처리하는 메서드 지정
3. @ RequestParam("name") String name
- URL에서 name이라는 쿼리 파라미터를 받아옴 (http://localhost:8080/hello-mvc?name=Spring -> name = "Spring")
4. Model model
- 뷰(HTML)로 데이터를 전달하는 역할을 함
- model.addAttribute("name", name); -> "name"이라는 변수를 Thymeleaf 템플릿에서 사용할 수 있도록 전달
5. model.addAttribute("name", name);
- name 값을 모델에 추가 -> 뷰에서 ${name}으로 접근가능
6. return "hello-template"
- Thymeleaf 템플릿 엔진을 사용하여 "hello-template"이라는 HTML 파일을 반환
- src/main/resources/templates/hello-template.html 을 찾음
- 만약 hell-template.html 파일이 있다면 그 파일을 클라이언트에게 보여줌
(View Resolver를 사용함)
API
@GetMapping("hello-api")
@ResponseBody
public Hello helloApi(@RequestParam("name") String name) {
Hello hello = new Hello();
hello.setName(name);
return hello;
}
@ResponseBody를 사용하면 View Resolver를 사용하지 않음, 대신 HTTP의 BODY에 객체를 반환 (객체는 JSON으로 변환됨)
@ResponseBody 사용
- HTTP의 BODY에 문자 내용을 직접 반환
- viewResolver 대신 HttpMessageConverter가 동작
- 기본 문자처리는 StringHttpMessageConverter가 동작
- 기본 객체처리는 MappingJackson2HttpMessageConverter가 동작
- byte 처리 등 기타 여러 HTtpMessageConverter가 기본으로 등록되어 있다.
컴포넌트 스캔과 자동 의존관계 설정
Controller가 Service와 Repository를 사용할 수 있게 의존관계를 준비하는 과정
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
회원 컨트롤러에 의존관계 추가한 코드
- 생성자에 @Autowired가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어준다.
- 이렇게 객체의 의존관계를 외부에서 넣어주는것을 DI, 의존성 주입이라고 한다.
- 이전 테스트 코드에선 직접 주입하는 방식을 사용했으나 여기선 @Autowired에 의해 스프링이 주입해준다.
(블로그엔 예제코드 생략했음. new로 직접 주입한 코드 참고)
스프링 빈을 등록하는 2가지 방법
- 컴포넌트 스캔과 자동 의존관계 설정
- 자바 코드로 직접 스프링 빈 등록
컴포넌트 스캔 원리
- @Component 어노테이션이 있으면 스프링 빈이 자동으로 등록된다.
- @Controller가 스프링 빈으로 등록된 이유도 컴포넌트 스캔 때문이다.
- @Service, @Repository도 자동으로 등록된다.
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
회원 Service에 스프링 빈 등록한 코드
- 생성자에 @Autowired를 사용하면 객체 생성 시점에 스프링 컨테이너에서 해당 스프링 빈을 찾아서 주입한다.
- 생성자가 1개만 있으면 @Autowired 생략 가능
@Repository
public class MemoryMemberRepository implements MemberRepository {
MemberRepository의 구현체인 MemoryMemberRepository에도 스프링 빈 등록을 했다.
이로써 컴포넌트 스캔에 의해 컨트롤러, 서비스, 리포지토리가 연결되었다.
- 스프링 빈을 등록할 땐 기본으로 싱글톤으로 등록한다. 따라서 같은 스프링 빈이면 모두 같은 인스턴스이다.
싱글톤이 아니게 설정할 수 있지만 대부분 싱글톤을 사용한다.
싱글톤 : 클래스의 인스턴스를 오직 하나만 생성하도록 보장하는 디자인 패턴, 즉 하나의 객체를 애플리케이션 전역에서
공유하며 한 번 생성된 객체를 재사용하는 방식 (나중에 추가로 공부)
자바 코드로 직접 스프링 빈 등록하기
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
- 회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowired 를 제거했다.
- 구현 클래스를 변경해야할 경우엔 스프링 빈으로 등록하는게 더 좋은 방법이다.
이 포스트엔 생략되어있지만, 비즈니스 요구사항엔 아직 DB가 선정되지 않았기 때문에 메모리로 만들고 나중에 교체하기로 했다.
이런 경우엔 자바 코드로 직접 스프링 빈을 등록해서 관리하는게 훨씬 수월하다.
컴포넌트 스캔을 사용하면 여러 코드를 변경해야하는 일이 발생하는데, 스프링 빈으로 등록하게 되면
public MemberRepository memberRepository() 메서드의 리턴값만 변경해주면 된다.
웹 MVC 개발
홈 화면 추가
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
}
localhost:8080에 접속했을 때 나오는 메인 화면 코드
이전에 했던 index.html 화면은 왜 안나오냐?
- 우선순위가 있다. 스프링 컨테이너에서 관련 컨트롤러를 찾지 못했을 경우에 static 파일을 찾게 되어있다.
이 상황에선 매핑된게 있기때문에 컨트롤러를 찾아서 home 페이지를 찾아서 화면을 출력하게 되는 것
회원 등록, 조회
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@GetMapping("/members/new")
public String createForm() {
return "members/createMemberForm";
}
@PostMapping("/members/new")
public String create(MemberForm form) {
Member member = new Member();
member.setName(form.getName());
memberService.join(member);
return "redirect:/";
}
@GetMapping("/members")
public String list(Model model) {
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
}
pulic String Create(MemberForm form)
- 웹에서 사용자가 입력한 값을 Spring이 MemberForm 객체로 변환
- form.getName()을 통해 입력한 값을 가져옴
- 새로운 Member 객체 생성
- setName()을 사용해 값을 DB에 저장
즉 view에서 등록한 이름을 getName으로 가져오고, 그 값을 setName으로 DB에 할당함.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<div>
<table>
<thead>
<tr>
<th>#</th>
<th>이름</th>
</tr>
</thead>
<tbody>
<tr th:each="member : ${members}">
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /container -->
</body>
</html>
public String list(Model model)
- 회원 목록을 가져옴
- Model에 "members"라는 이름으로 키 저장.
- 이 "members"는 타임리프의 ${members}에서 사용하는 것
- th:each="member :${members} -> 리스트 데이터를 순회하여 출력
JDBC Repository 구현
public class JdbcMemberRepository implements MemberRepository {
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
//이하 코드 생략
@Configuration
public class SpringConfig {
private DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
}
}
* DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체이다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로
DataSource를 생성하고 스프링 빈으로 만들어 준다. 그래서 DI를 받을 수 있다.
- 자바 코드로 직접 스프링 빈을 등록한 SpringConfig 클래스이다.
여기서 새로 구현한 JdbcMemberRepository를 리턴해주게 변경하고, DataSource를 생성자 주입했다.
기존 MemoryMemberRepository를 JdbcMemberRepository로 단순히 갈아끼운것이다.
OCP (개방-폐쇄 원칙)
- 확장에는 열려있고, 수정/변경에는 닫혀있다.
- 스프링의 DI를 사용하면 기존 코드를 전혀 손대지 않고 설정만으로 구현 클래스를 변경할 수 있다.
스프링 통합 테스트
@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
@Test
void 회원가입() {
// given
Member member = new Member();
member.setName("spring");
// when
Long saveId = memberService.join(member);
// then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복_회원_예외() {
//given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
- @SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행하는 어노테이션
- @Transactional : 테스트 케이스에 이 어노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후엔
항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.
(단위 테스트에서 했던 @BeforeEach, @AfterEach를 떠올려보자)
'Spring' 카테고리의 다른 글
스프링 핵심 원리 (2) (0) | 2025.02.20 |
---|---|
스프링 핵심 원리(1) (0) | 2025.02.19 |
스프링 - IoC (0) | 2025.02.05 |
Maven 이란? (0) | 2024.08.28 |
Path Variable과 Request Param (0) | 2024.08.06 |