이번에 Spring에 대해 공부하게 되었는데 내용을 정리해봐야겠다.
1. 웹 애플리케이션 계층 구조
- Domain : 엔티티 선언을 통해 DB에 저장되는 객체들을 구현한다.
즉, 테이블의 각 Column들이 하나의 도메인이라 보면 된다. - Repository : 데이터베이스에 직접적으로 접근해 도메인 객체를 DB에 저장하고 관리한다.
보통 Interface로 만들고 JpaRepository를 상속받아 사용한다.
SQL문을 직접 입력할 수도 있다. - Controller : 웹 MVC의 컨트롤러 역할, Client가 요청을 하면 그 요청을 실질적으로 수행하는 서비스를 호출한다.
- Service : 핵심 비즈니스 로직을 구현, 주로 리포지토리를 이용해 CRUD을 구현한다.
2. Domain
package hello.hellospring.domain;
public class Member {
private Long id;
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;
}
}
Id와 이름에 대한 정보를 가지는 멤버 엔티티이다.
3. Repository
멤버 repository 인터페이스
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
만약 Service에서 DB에 있는 멤버에 관한 정보를 찾거나 수정, 삭제 하게 된다면 위 Repository를 이용한다.
하지만 위 예시는 Spring JPA를 사용하지 않아 직접 구현을 해주어야 한다.
멤버 repository 구현
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
public void clearStore() {
store.clear();
}
}
위와 같이 직접 구현해 주어야 한다. 하지만 Spring JPA를 사용한다면
Spring JPA 이용한 코드
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long>{
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
구현할 필요없이 위처럼 적기만 하면 된다... Spring JPA는 다음에 자세히 정리해 봐야겠다.
4. Service
회원 서비스
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
private final MemberRepository memberRepository = new
MemoryMemberRepository();
/**
* 회원가입
*/
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("이미 존재하는 회원입니다.");
});
}
/**
* 전체 회원 조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
join은 회원가입, validateDuplucateMember는 회원 중복 확인
findMembers는 모든 멤버찾기, findOne은 해당하는 Id를 가지는 멤버 찾기이다.
코드를 보면 Repository를 이용해 DB에 접근하는 것을 볼 수 있다.
여기서 왜 굳이 Repository를 이용해 DB를 접근할까? 그냥 Service에서 바로 접근하면 안될까? 라는 의문이 들었다.
알아보니 DB에 바로 접근하게 되면 잘못된 코드로 인해 DB 내용이 예상하지 못하게 바뀔 위험이 있기 때문이라고 한다.
DB에 접근하는 용도인 Repository를 만들어 DB에 접근하는 메소드들을 Repository에 딱 정해놓고 이 메소드들만 이용하게 하면 위험이 휠씬 줄어든다.
5. Controller
package hello.hellospring.controller;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@GetMapping(value = "/members/new")
public String createForm() {
return "members/createMemberForm";
}
@PostMapping(value = "/members/new")
public String create(MemberForm form) {
Member member = new Member();
member.setName(form.getName());
memberService.join(member);
return "redirect:/";
}
@GetMapping(value = "/members")
public String list(Model model) {
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
}
@Autowired는 스프링이 연관된 객체를 스프링 컨테이너에서 알아서 찾아서 넣어준다.
나머지 @GetMapping 과 @PostMapping은 각 요청을 구분하는 것으로 GET 요청이 오는지 POST 요청이 오는지 구분한다. 뒤에 value에 들어가있는 주소로 해당하는 요청이 오면 해당 함수를 호출한다.
예를 들어 로컬 주소가 https://localhost:8080이라고 할 때, https://localhost:8080/member/new 로 Get 요청이 온다면
createForm 함수로 호출이 되고, Post 요청이 온다면 create 함수가 호출될 것이다.
6. 정리
Controller는 요청이 들어오면 해당하는 요청과 주소에 맞는 Service 함수를 호출한다.
Service는 해당하는 Repository 기능을 이용해 DB 작업을 수행한다.
Repository는 실제로 DB에 접근한다. 여기서 Spring JPA를 사용한다면 대부분의 SQL문은 Spring이 알아서 해준다.
Spring JPA말고도 Entity Manager를 사용하는 방법이 있는데 다음에 정리해 봐야겠다.
'개념 정리 > Spring' 카테고리의 다른 글
[Spring] ShedLock을 통해 서버간 스케줄 중복 수행 방지 (0) | 2023.07.29 |
---|---|
[Spring] @Scheduled 이용해 특정 시간마다 함수 호출하기 (0) | 2023.07.24 |
[Spring Security] 기본 로그인 화면 제거 (0) | 2023.07.23 |
[Spring] UUID와 increment PK를 언제 사용할까? (0) | 2023.07.19 |
[Spring] 스프링 DTO (0) | 2023.07.03 |