스프링부트 프로젝트의 구조
src/main/java 디렉토리
- controller, dto, service, domain 등등의 자바 파일
[프로젝트명]+Application.java
package com.mysite.sbb; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class StudyApplication { public static void main(String[] args) { SpringApplication.run(StudyApplication.class, args); } }
@SpringBootApplication: 스프링부트의 모든 설정을 관리
src/main/resources 디렉토리
- 자바 파일을 제외한 HTML, CSS, javascript 환경 파일을 작성하는 공간
src/test/java 디렉토리
- 프로젝트에서 작성한 파일을 테스트하기 위한 테스트 코드 작성의 공간. JUnit, 스프링 부트의 테스팅 도구를 사용해 서버를 실행하지 않은 상태에서 src/main/java 디렉토리에 작성한 코드를 테스트할 수 있다.
build.gradle 파일
- Gradle이 사용하는 환경 파일. Gradle은 빌드도구, 플러그인과 라이브러리 등을 기술
목표1: hello world!
application.properties
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQLDialect spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/gdsc_db?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true spring.jpa.hibernate.ddl-auto=create
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
컨트롤러
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class MainController { @GetMapping("/first") @ResponseBody public String index() { return "hello world!"; } }
MVC 패턴을 사용하는 스프링에서, controller는 View와 Model을 연결시키는 다리역할
ex) View 에서 /first로 가줘 하고 요청하면 어디로 갈지 분석하고 올바른 길로 연결시켜 준다. (Service에 연결되는게 일반적이다.)
@Controller
: MainController 클래스를 스프링부트의 컨트롤러로 만들어줍니다.
@GetMapping
: 요청된 url(현재 “localhost:8080/first”)과의 매핑을 담당
@ResponseBody
: URL 요청에 대한 응답으로 문자열을 리턴하라는 의미
목표2: 간단한 멤버 회원가입, 리스트 확인하기
- 저번 시간에 배웠던 domain, repository으로 memeber 도메인과 memeberRepository 만들기
Member.java
package gdscstudy.demo.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; //spring boot 2.x대는 jakarta 대신 javax를 써야함. @Entity public class Member { @Id @GeneratedValue private Long id; // 구분하기 위해 시스템이 저장하는 아이디 @Column private String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName(){ return name; } public void setName(String name){ this.name = name; } }
MemberRepository
package gdscstudy.demo.repository; import gdscstudy.demo.domain.Member; import org.springframework.data.repository.CrudRepository; import java.util.List; import java.util.Optional; public interface MemberRepository extends CrudRepository<Member, Long> { Member save(Member member); Optional<Member> findById(Long id); // id로 회원 찾기 Optional<Member> findByName(String name); // 없으면 null이 반환되는데, 그렇게 반환하지 않고 Optional로 감싸서 반환한다. List<Member> findAll(); // 지금까지 저장된 모든 회원 리스트 반환. }
Optional<> : null 이 반환될 수 있는 값을 감싸는 wrapper 클래스.
→ 사용 이유: null값을 참조해 발생하는 null pointer exception 등의 에러를 발생하지 않도록 도와준다.
- thymeleaf
members/createMemberForms.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <body> <div class="container"> <form action="/members/new" method="post"> <div class="form-group"> <label for="name">이름</label> <input type="text" id="name" name="name" placeholder="이름을 입력하세요"> </div> <button type="submit">등록</button> </form> </div> </body> </html>
members/memberList.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Member List</title> </head> <body> <div class="container"> <div> <table> <thread> <tr> <th>#</th> <th>이름</th> </tr> </thread> <tbody> <tr th:each="member : ${members}"> <td th:text="${member.id}"></td> <td th:text="${member.name}"></td> </tr> </tbody> </table> </div> </div> </body> </html>
home.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div class="container"> <div> <h1>Hello Spring</h1> <p>회원 기능</p> <p> <a href="/members/new">회원 가입</a> <a href="/members">회원 목록</a> </p> </div> </div> </body> </html>
MemberController
package gdscstudy.demo.controller; import gdscstudy.demo.domain.Member; import gdscstudy.demo.dto.MemberDto; import gdscstudy.demo.service.MemberService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import java.util.List; @Controller public class MemberController { // 멤버 서비스를 스프링이 스프링 컨테이너에 있는 멤버서비스를 가져와서 딱 붙여줌. @Autowired private MemberService memberService; @GetMapping("/members/new") public String createForm(){ return "members/createMemberForms"; } @PostMapping("/members/new") public String create(MemberDto memberDto){ Member member = new Member(); member.setName(memberDto.getName()); // 서비스 System.out.println("member= "+ member.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"; } @GetMapping("/") public String home(){ return "home"; } }
Service
- Controller: api 만들 때 컨트롤
- Service: 동일한 아이디로는 중복가입이 안된다는 등의 비즈니스 로직들이 들어가있음 ( 이 비즈니스 도메인 객체를 가지고 핵심 비즈니스 로직이 동작하도록 구현한)
- Domain: 회원, 주문, 쿠폰처럼 디비에 주로 저장되고 관리되는 비즈니스 도메인 객체
- Repository
결국 서비스는 핵심 비즈니스 로직이 동작하도록 구현된 부분입니다.
서비스가 필요한 이유
- 모듈화
예를들어 어떤 컨트롤러가 여러개의 리포지터리를 사용하여 데이터를 조회한후 가공하여 리턴한다고 가정해 보자. 이러한 기능을 서비스로 만들어 두면 컨트롤러에서는 해당 서비스를 호출하여 사용하면 된다. 하지만 서비스로 만들지 않고 컨트롤러에서 구현하려 한다면 해당 기능을 필요로 하는 모든 컨트롤러가 동일한 기능을 중복으로 구현해야 한다. 이러한 이유로 서비스는 모듈화를 위해서 필요하다.
- 보안
컨트롤러는 리포지터리 없이 서비스를 통해서만 데이터베이스에 접근하도록 구현하는 것이 보안상 안전하다. 이렇게 하면 어떤 해커가 해킹을 통해 컨트롤러를 제어할 수 있게 되더라도 리포지터리에 직접 접근할 수는 없게 된다.
package gdscstudy.demo.service; import gdscstudy.demo.domain.Member; import gdscstudy.demo.repository.MemberRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.Optional; @Service public class MemberService { @Autowired private MemberRepository memberRepository; /* * 회원 가입 * */ public Long join(Member member){ // 같은 이름이 있는 중복 회원은 안됨. validateDuplicateMember(member); // 중복 회원 검증 memberRepository.save(member); return member.getId(); } private void validateDuplicateMember(Member member) { memberRepository.findByName(member.getName()).ifPresent(m ->{ throw new IllegalStateException("이미 존재하는 회원입니다."); }); // result가 optional로 감싸져서, 어떤 값이 있으면 이게 동작하는 것임. 만약 아녔으면 ifNull이라고 했겠지만 (이건 과거임) 옵셔널로 감쌌기 때문에 이렇게! // m 에 값이 있으면 throw } /** * 전체 회원 조회 * */ public List<Member> findMembers(){ return memberRepository.findAll(); } public Optional<Member> findOne(Long memerId){ return memberRepository.findById(memerId); } }
Dto
계층 간 데이터를 주고 받는 DTO(Data Access Object - 데이터 전송 객체)
그냥, domain 객체 등의 정보를 바로 넘겨도 되는거 아닌가?
! 비추
Separation of Concerns
: 관심사의 분리
→ 코드 작성 시 하나의 역할 별로 분리해서 작성하자는 원칙
⇒ 결국, Low Coupling, High Cohesion
마틴 파울러가 DTO를 소개하며, DTO는
“한 번의 호출로 여러 매개 변수를 일괄 처리해서 서버의 왕복을 줄이는 것”이라 하였습니다.
→ 해당 호출에 관련된 모든 데이터를 가지고 있는 DTO 객체를 만들어서 네트워크 비용을 줄인다는 의미
그렇기에 DTO로 분리한다.
package gdscstudy.demo.dto; public class MemberDto { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }