환경세팅하는 부분은 이 포스팅에서 볼 수 있다.
아직 이 코드들은 github에 올리지는 않았다.
github에 올리게 된다면 포스팅 수정 후 추가하겠다.
MVC 디자인패턴을 최대한 준수하기 위해 위와 같이 Domain, Repository, Controller, Service로 나누어서 코드를 짰다.
회원 엔티티에 있어야 할 정보들을 살펴보기 위해 Domain부터 살펴보자.
(참고로 Entity: JPA가 관리하는 클래스라고 생각하면 된다. 즉, 회원 클래스는 jpa에서 관리하는 클래스이므로 Member 클래스를 Member 엔티티라고 부르겠다.)
Domain
관리하는 db테이블이 member 분이므로 Member 엔티티밖에 없다. 바로 Member 클래스를 살펴보자.
Member.class
package shop210504.pracshop.domain;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
@Getter @Setter
@RequiredArgsConstructor
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String tier;
}
@Entity 를 추가하여 JPA에서 다루는 엔티티임을 명시해준다.
이 어노테이션을 추가하면 Mysql에서 Member 테이블과 Java에서 Member.class가 서로 연동돼있음을 JPA에서 알 수 있다. 회원 정보로는 회원넘버, 회원이름, 회원티어를 새겨주었다.
회원 넘버 위에 @Id는 db에서의 PK(Primary Key, 기본키)임을 명시해준 것이고, @GeneratedValue는 넘버링이 알아서 자동증가하여 번호를 매겨주게 한다는 뜻이다.
Repository
객체지향 설계를 위해 MemberRepository 인터페이스와 그 구현체에 해당하는 MemberRepositoryImpl 클래스로 나누어 코딩하였다. SOLID 원칙의 OCP(개방-폐쇄 원칙)을 잘 지킬 수 있게 된다. 더 자세한 건 나도 아직 잘 모르겠다... 확실한건 기능추가될 때 수정을 최소화할 수 있다.
MemberRepository.class (Interface)
package shop210504.pracshop.repository;
import shop210504.pracshop.domain.Member;
import java.util.List;
public interface MemberRepository {
Member save(Member member);
Member findMember(Long id);
void deleteById(Long id);
List<Member> showMembers();
}
회원 저장을 위한 save 메소드,
회원 삭제 및 정보수정에 쓰이는 회원탐색 findMember 메소드,
회원 리스트 출력을 위해 쓰이는 showMembers 메소드,
회원 삭제 deleteById 메소드가 있다.
구현체에는 위 코드들을 오버라이딩할 것이다.
MemberRepositoryImpl.class (Implementation)
package shop210504.pracshop.repository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import shop210504.pracshop.domain.Member;
import javax.persistence.EntityManager;
import java.util.List;
@Repository
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepository{
private final EntityManager em;
@Override
public Member save(Member member) {
em.persist(member);
return member;
}
@Override
public Member findMember(Long id) {
Member findMember = em.find(Member.class, id);
return findMember;
}
@Override
public void deleteById(Long id) {
Member member = findMember(id);
em.remove(member);
}
@Override
public List<Member> showMembers() {
//System.out.println("Repository의 showMembers 호출");
return em.createQuery("select m from Member m", Member.class).getResultList();
}
}
EntityManager를 통해 JPA ORM을 사용하게 했다.
JPA 명령어 (em.persist, em.find, em.remove 등)은 아래 포스팅을 참고하자.
(참고로 em.merge를 하면 update도 할 수 있을 듯한데, 굳이 그럴 필요가 없다.)
또한, @Repository 어노테이션을 붙여주면 스프링부트에서 의존관계 주입을 통해
이 클래스가 리포지토리로 쓰이며, 다른 객체에서 리포지토리 로직을 필요로 할 때, 이 어노테이션이 붙여진 클래스의 메소드들이 자동으로 이용된다.
@RequiredArgsContructor 역시 의존관계 자동 주입에 사용되며, 생성자를 자동으로 lombok이 생성해주게 하는 어노테이션이다.
Service
db에 회원 정보들이 저장돼있고, 그에 대한 접근을 repository 패키지에서 한다면,
이와 관련된 비즈니스 로직은 Service 패키지에서 작성한다고 보면 된다.
지금 이 코드는 Member 회원조회밖에 없으므로 Service, Repository가 별 차이가 없어보인다.
아래 질문글이 있는 걸 보아하니, 나만 이렇게 느낀 것이 아니었나보다.
www.inflearn.com/questions/77417
질문글에 남겨진 답변이 (아직은 잘 감은 안오지만) 많은 도움이 되었다.
Service 또한 Repository처럼 MemberService 인터페이스와 그 구현체인 MemberService.Impl 클래스로 나누어 코딩하였다.
MemberService.class (Interface)
package shop210504.pracshop.service;
import shop210504.pracshop.domain.Member;
import java.util.List;
public interface MemberService {
Long join(Member member);
Member find(Long id);
void delete(Long id);
List<Member> showMembers();
}
MemberServiceImpl.class (Implementation)
package shop210504.pracshop.service;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import shop210504.pracshop.domain.Member;
import shop210504.pracshop.repository.MemberRepository;
import java.util.List;
@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
@Override
public Long join(Member member) {
Member joinMember = memberRepository.save(member);
return joinMember.getId();
}
@Override
public Member find(Long id) {
Member member = memberRepository.findMember(id);
return member;
}
@Override
public void delete(Long id) {
memberRepository.deleteById(id);
}
@Override
public List<Member> showMembers() {
List<Member> members = memberRepository.showMembers();
System.out.println("ServiceImple의 showMembers 호출");
for (Member member : members) {
System.out.println("member.getName() = " + member.getName());
}
return members;
}
}
솔직히 아직은 Service와 Repository가 차이점이 없기 때문에
Service에 해당하는 로직은 전부 Repository의 메소드를 불러서 해결하고 있다.
그도 그럴것이, 지금 CRUD 기능만 실습하고 있기 때문이다.
Controller
회원정보들이 조회되는 화면만 보여주면 되므로 Controller에는 많이 적을 것이 없다.
IndexController.class
package shop210504.pracshop.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import shop210504.pracshop.domain.Member;
import shop210504.pracshop.service.MemberService;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class IndexController {
private final MemberService memberService;
@GetMapping("/member")
public String home(Model model){
List<Member> memberList = memberService.showMembers();
model.addAttribute("members", memberList);
return "index";
}
}
주소창 localhost:8080 뒤에 @GetMapping에 적혀있는 /member 를 붙이면
IndexController의 "/member"와 매핑된 메소드가 호출돼
Service 객체의 showMembers() 메소드를 통해 회원정보들을 부른 후,
스프링부트 model에 "members"라는 attributeName으로 데이터를 저장한 후,
return "index" 를 통해 index.html을 호출한다.
<!DOCTYPE html>
<html lang="en">
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>회원조회 연습 홈페이지</title>
<link href="../static/css/index.css" th:href="@{/css/index.css}" rel="stylesheet" />
</head>
<body>
<h1>너가 관리하는 회원들이 궁금해?</h1>
<a href="/login">로그인</a><br>
<a href="/createMemberForm">회원가입</a>
<hr>
<div class="container">
<table width="100%" border="1">
<thead>
<tr>
<th>회원번호</th>
<th>이름</th>
<th>티어</th>
<th>정보 수정</th>
</tr>
</thead>
<tbody>
<tr th:each="member : ${members}">
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
<td th:text="${member.tier}"></td>
<td><a href="/" th:href="@{'delete/'+${member.id}}">삭제</a></td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
attributeName에 해당하는 ${members} 에 List<Member> 가 저장돼있는 것이며,
thymeleaf 템플릿 엔진으로 멤버정보들을 출력해준다.
결과
TDD를 통해 Member 회원가입, 회원조회가 잘 일어남을 확인할 수 있었다.
자세한 건 아래 포스팅을 참고하자.
그래도 의심이 되는 분들을 위해 직접 결과를 보자.
choisk0531이 잘 들어가 있음을 확인할 수 있다.
(@Rollback(false)로 해놓고 test를 많이 돌렸더니 choisk0531이 분신술을 써버린 것처럼 결과가 나왔다...)
MySQL에 또한 잘 들어가 있음을 확인했다.
(확인하고 바로 delete from member where name="choisk0531"; 쿼리 날렸다 ㅎㅎ)
간단하게 회원조회 코드를 짜보았다!
사실 spring, jpa가 기본적으로 공부할 게 많아
여기까지 오는데도 꽤 시간이 걸릴 수 있다.
앞으로 남은 시간동안 꾸준히 spring boot, jpa, java를 공부한다면
내 실력에 더 많은 발전이 이루어지지 않을까 기대한다.
'JAVA > JAVA | Spring 학습기록' 카테고리의 다른 글
[Spring] 스프링이 태어난 이유_서블릿, JSP로 만든 MVC의 한계 (0) | 2021.07.06 |
---|---|
[Spring] lombok의 @Builder와 JpaRepository를 도입해보았다 (0) | 2021.05.17 |
[Spring] CRUD 기능 실습 중 @DeleteMapping 관련 에러 (0) | 2021.05.11 |
[Spring] Spring boot Test 코드 작성해보기 (TDD 연습) (2) | 2021.05.05 |
[Spring] MySQL+JPA+Spring+Gradle 환경설정 및 실습 (0) | 2021.05.04 |