IT recording...
[Spring] 회원 관리 예제 (Controller, Service, Repository, Domain의 역할) + Singleton,DI,IoC/ Optional,Assertions / JUnit test 본문
[Spring] 회원 관리 예제 (Controller, Service, Repository, Domain의 역할) + Singleton,DI,IoC/ Optional,Assertions / JUnit test
I-one 2021. 3. 8. 14:48
1. 비즈니스 요구사항 정리
* 데이터 : 회원ID(자동생성), 이름
* 기능 : 회원 등록, 조회
* 아직 데이터 저장소가 선정되지 않음
2. 기본 웹 어플리케이션 계층 구조
- Controller : 웹 MVC의 컨트롤러 역할
- Service : 핵심 비즈니스 로직 구현
- Repository : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- Domain : 비즈니스 도메인 객체, ex) 회원,주문,쿠폰 등등 주로 데이터베이스에 저장하고 관리됨
Controller -> Service -> Repository -> DB
|
V
Domain
(4개의 정확한 역할에 대한 느낌이 와닿지 않는다. 아래의 예제를 통해 느낌을 확실히 해보자)
3. 코드 작성하기
1) Controller
- 웹 사이트에서 받은 사이트 주소를 Mapping 시키고, 받은 정보들 Service에 전달해주는 배달원 역할을 수행한다.
(주소 찾아서 정보 전달)
controller/HomeController
//HomeController
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
}
// templates/home.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<div>
<h1>Hello Spring</h1>
<p>회원 기능</p>
<p>
<a href="/members/new">회원 가입</a>
<a href="/members">회원 목록</a>
</p>
</div>
</div> <!-- /container -->
</body>
</html>
controller/MemberController
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService){
this.memberService = memberService;
}
//GetMapping("/members/new")로 들어오면 templates/members/createMemberForm 주소 찾아서 이동 시켜 줌
//> createMemberForm에서 Post로 넘긴 정보를 @PostMapping("/members/new")로 받아서 처리
@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:/"; //home으로
}
//GetMapping("/members")로 들어오면 templates/members/memberList로 이동
//model.addAttribute의 key,value형태로 정보 전달
@GetMapping("/members")
public String list(Model model){
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
}
// templates/members/createMemberForm.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> <!-- /container -->
</body>
</html>
// templates/members/memberList.html
<!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}"> <!--th:each = 루프 돌면서 로직 실행-->
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /container -->
</body>
</html>
* model을 통한 데이터 전달 관련 블로그
2) Domain
- 쉽게 말하면 객체. 회원(id,name)/ 주문(id,order,member) /쿠폰(id,store,member) 등.
- DB에 저장되어 관리되는 애들을 말한다.
domain/Member
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;
}
}
3) Repository
- 직접 DB를 건드는 애들. save, delete, find 등의 일이 수행된다.
- 아직 db가 확정되지 않은 상황을 가정했으므로, 코드 바꿔치기가 수월하게 interface를 사용해서 작성하였다.
repository/MemberRepository
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
repository/MemoryMemberRepository
@Repository
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
//sequence이용해서 id 관리(auto increment)
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(),member);
return member;
}
//Optional이라는 반환형을 사용한다. (Null값 처리에 용이)
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
//자바의 람다식
//map.values() : map에 존재하는 모든 value들 집합 return
//.stream() : iterator한 작업을 수행 가능하게 해준다. + 병렬처리 가능
//.filter() : 람다식을 이용해서 필요한 정보만 뽑아내기
//.findAny() : 찾은 것 중 맨 처음꺼 꺼내기
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
//Test시 저장소의 정보들을 정리해주기 위한 메소드
public void clearStore(){
store.clear();
}
}
- Optional 자료형
: 예상치 못한 Null 관련 예외를 회피할 수 있다.
> 생성 : of(), ofNullable(value), empty() 메소드를 사용하여 Optional 객체를 생성한다.
> 접근 : get(), orElse(T other), orElseGet()
// ofNullable, orElse 사용
Optional<String> maybeCity = Optional.ofNullable(cities.get(4)); // Optional
int length = maybeCity.map(String::length).orElse(0); // null-safe
System.out.println("length: " + length);
//null값이 반환될 수 있는 상황에서의 사용
public static <T> Optional<T> getAsOptional(List<T> list, int index) {
try {
return Optional.of(list.get(index));
} catch (ArrayIndexOutOfBoundsException e) {
return Optional.empty();
}
}
//ifPresent 사용 (람다식과 콜라보레이션)
Optional<String> maybeCity = getAsOptional(cities, 3); // Optional
maybeCity.ifPresent(city -> {
System.out.println("length: " + city.length());
});
Optional 관련 블로그
- stream 관련 부가 정보
4) Serive
- 회원 가입, 조회 등의 비즈니스 단 기능들이 수행되는 곳. 중복 검사 등도 이 곳에서 실행된다. 직접 DB에 접근하는 일은 없다. (repository를 거치기 때문)
service/MemberService
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
/**
* 회원가입
*/
public Long join(Member member) {
validateDuplicateMember(member); //중복 회원 검증
memberRepository.save(member);
return member.getId();
}
//ctrl+ alt + shift + T : 여러가지 나옴
//ctrl + alt + m : 메소드 뽑아내기
//같은 이름이 있는 중복 회원X
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);
}
}
4. Test하기
- 자바에는 클래스 별로 test를 할 수 있는 환경이 존재한다.
실제 코드 빌드에는 들어가지 않으나 작은 단위로 쪼개어 프로그램이 정상 작동하는지를 확인할 수 있다.
- @Test 태그를 이용하여 식별하는데, 작성된 Test코드 중 어느 것이 먼저 작동할 지는 랜덤이다.
따라서 같은 repository를 사용하는 경우 한 method가 실행 된 후 저장소를 깔끔히 청소해 놓아야 다음 method가 실행시켰을 때 정상 작동 테스트가 정상적으로 가능하다.
--> @AfterEach 태그를 통해 각 method들이 끝난 후 메모리를 정리할 수 있는 코드를 작성했다.
- Assertions
: 조건문을 단순화하고 반복적인 코드를 줄이는 역할을 한다.
assertEqulas(expected,result), assertNull(x), assertNotNull(x) 등
test/java/hello.hellospring.repository/MemoryMemberRepositoryTest
public class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
//test가 끝난 후 메모리 정리해주는 메소드 , 테스트들 중 어떤 것이 먼저 실행될 지는 보장 되지 않는다.
@AfterEach
public void afterEach(){
repository.clearStore();
}
@Test
public void save(){
Member member = new Member();
member.setName("spring");
repository.save(member);
Member result = repository.findById(member.getId()).get();
Assertions.assertEquals(member,result);
}
@Test
public void findByName(){
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
//shift+F6 : rename편하게
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
Member result = repository.findByName("spring1").get();
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll(){
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
Member member3 = new Member();
member3.setName("spring3");
repository.save(member3);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(3);
}
}
test/java/hello.hellospring.service/MemberServiceTest
class MemberServiceTest {
//아래와 같이 하면 객체간의 종속성이 존재하여, test와 실제 service에서 사용하는 repository가 같지 않게 될 수도 있다.
/*MemberService memberService = new MemoryMemberRepository();
MemoryMemberRepository memberRepository = new MemberService(memberRepository);*/
MemberService memberService;
MemoryMemberRepository memberRepository;
//BeforeEach 태그를 활용해 DI(dependency injection)의존성을 강제로 주입해준다.
//Service의 생성자를 통하여 repository를 강제로 주입
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
@Test
void join() {
//given
Member member = new Member();
member.setName("spring");
//when
Long saveId = memberService.join(member);
//then
Member findMember = memberService.findOne(saveId).get();
Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void duplicateException(){
//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("이미 존재하는 회원입니다.");
/*try{
memberService.join(member2);
fail();
}
catch(IllegalStateException e){
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다12.");
}*/
//then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
- Sigleton
: 어플리케이션 실행 시 특정 클래스가 메모리를 최초 한번만 할당하여, 그 메모리에 인스턴스를 만들어 사용하는 디자인 패턴.
+ 고정된 하나의 메모리를 사용하여 메모리 낭비 방지
+ 메모리 누수 방지
+ 공통된 오브젝트를 사용하는 상황에서 유용
1) Java singleton - classloader에 의해 싱글톤 패턴을 구현한다.
: 생성자 private선언(외부에서 클래스의 오브젝트를 생성할 수 없게 함)
: 참조는 static으로 정의(어느 영역에서든 접근이 가능하도록)
+ Thread safety를 위해 > getter 메소드의 Synchronized 선언
2) Spring singleton - spring container에 의해 구현한다.
해당 클래스에 대해 딱 한 개의 인스턴스를 만들며, bean이 호출될 때마다 스프링은 생성된 공유 인스턴스를 return시킨다.
: @Bean,@Controller,@Service,@Repository,@Component
+ 생성자에 @Autowired를 통해 컨테이너에서 스프링이 연관된 객체를 찾아 넣어준다.(DI)
- 과거에는 xml을 통해 작성했지만 최근에는 Annotation으로 흐름이 변경됨.
- SpringConfig 파일 작성을 통해 구현도 가능.
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService(){
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
(Controller는 와안전 기본으로 찾아서 하니까 여기에 작성할 필요 없음)
*참조 블로그
- DI (dependency injection, 의존성 주입)
: Spring의 IoC(제어의 역전) 으로, FrameWork에 의해 객체의 의존성이 주입되는 설계 패턴이다.
종속성이 감소하여 component들의 속성이 변경되어도 민감하지 않고, test가 용이해진다.
public class MemberService {
//아래와 같이 작성된다면 객체끼리의 종속성이 존재한다.
/*private final MemberRepository memberRepository = new MemoryMemberRepository();*/
//따라서 외부에서 종속성을 넣어주게끔 코드 변경.
private final MemberRepository memberRepository;
//@Autowired
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
}
*관련 블로그
'Spring' 카테고리의 다른 글
[Spring] GithubAction + S3 + CodeDeploy + NginX를 이용한 무중단 배포 (0) | 2022.01.13 |
---|---|
[Spring] 자동배포시 secret 관리 (0) | 2022.01.13 |
[Spring] Spring Security + Swagger2 연결 (0) | 2021.12.21 |
[Spring] 스프링 입문(Static, Dynamic, API) (0) | 2021.03.08 |
[Spring] 프레임워크로 Spring을 선택하게 된 이유 (0) | 2021.03.08 |