Spring 웹 애플리케이션 계층 구조
아무리 봐도 왜 controller, domain, repository, service라는 패키지를 만들었는지 이해를 할 수 없다.
따라서 더 깊이 파보려고 한다.
이 그림은 스프링의 웹 계층에 대해서 표현해둔것이다.
스프링의 웹 계층은 4가지로 나뉜다.
< 스프링 웹 계층 >
- Domain Model
- Presentation Layer(Controller)
- Business Layer(Service Layer)
- Data Access Layer(Reopsitory Layer)
Domain Model
일단 먼저 웹을 설계할때 도메인을 먼저 정하는게 중요하다.
Domain
도메인 모델(객체)은 내가 개발하고자 하는 영역을 분석하고,
그 분석의 결과로 도출된 모델(객체)이라고 할 수 있다.
온라인 쇼핑몰을 예를 든다면,
- 주문 (핵심 기능)
- 회원
- 결제
- 배송
- 리뷰 로 도메인을 나눌 수 있다. 이 도메인중 또 하위 도메인으로 나눌 수 있게된다.
우리가 작성한 코드로 보자면 여러 도메인들중의 하위 도메인인,
회원 도메인으로 도메인 모델을 작성했었다.
Entity와 Value
도출한 도메인 모델은 크게 entity와 value로 구분할 수 있다.
Entity
식별자를 가진다.
식별자 이외의 데이터가 변경이 되어도 그 객체가 다른 객체가 되는것이 아니다.
예를 들어서 Member는 member_id라는 식별자를 가진다고 해보자!
그러면 Member에서 닉네임을 변경한다고 해도 Member가 바뀌지 않는다.
DB의 entity와는 다른 개념이다. 여기서의 entity는 논리 모델에서 사용된다.
Value
식별자를 가지지 않고 값 그 자체이다.
value같은 경우에는 한 데이터가 변경되면 아예 다른 객체가 되어버린다.
따라서 value를 immutable로 구현하는게 좋다.
이를 위해 값을 생성자를 통해서만 받고 setter를 구현하지 않는다.
기존 객체를 변경하고 싶으면 아예 새로운 객체를 만든다.
데이터베이스 연동 방식
지금까지 Domain, Entity, Value에 대해서 알아보았다.
결국 이 데이터 객체들을 db에 저장을 해야한다.
그런데 이 데이터 객체들을 저장하는 방식에 따라서 데이터 객체가 Entity로 불릴 수 도 있고,
Value Object로 불릴 수 도 있다.
JPA
데이터 객체: Entity
ORM
SQL문이 아닌 RDB 객체를 자바 객체로 매핑
객체간 관계, 식별자를 가질 수 있다.
따라서 식별자를 가지고 있는 Entity가 된다.
MyBatis
데이터 객체: VO(ValueObject)
SQL-Mapper
SQL문으로 RDB에 접근하고 데이터를 객체로 매핑
체간 관계나 식별자는 가질 수 없다.
따라서 식별자를 가지고 있지 않은 VO(Value Object)가 된다.
CODE
hello/hellospring/controller/HelloController.java
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 식별자 멤버 이름이 들어가야한다.
Repository Layer (Data Access Layer)
JPA, ORM(Mybatis, Hibernate)를 주로 사용하는 계층이다.
DAO 인터페이스와 @Repository 애노테이션을 사용하여 작성된 DAO 구현 클래스가 이 계층에 속한다.
Database에 Data를 CRUD(Create, Read, Update, Drop)하는 계층이기도 하다.
DAO(Data Access Object) DB에 접근하는 객체,
DB를 사용해 데이터를 조작하는 기능을 하는 객체 (MyBatis 사용시에 DAO or Mapper 사용)
Repository라고도 부름(JPA 사용시 Repository 사용)
Service 계층과 DB를 연결하는 고리 역할을 한다.
CODE
hello/hellospring/repository/MemberRepository.java
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();
}
인터페이스는 틀을 미리 짜주는 역할을 하는데(템플릿 같은 느낌),
repository 를 짤때 필요한 함수들을 만들었다.
여기서 Optional이라는게 있는데, 무엇인가?
NPE(NullPointerException)이란?
개발을 할 때 가장 많이 발생하는 예외 중 하나가 바로 NPE(NullPointerException)이다. NPE를 피하려면 null 여부를 검사해야 하는데, null 검사를 해야하는 변수가 많은 경우 코드가 복잡해지고 번거롭다. 그래서 null 대신 초기값을 사용하길 권장하기도 한다.
Optional는 null이 올 수 있는 값을 감싸는 Wrapper 클래스로, 참조하더라도 NPE가 발생하지 않도록 도와준다. Optional 클래스는 아래와 같은 value에 값을 저장하기 때문에 값이 null이더라도 바로 NPE가 발생하지 않으며, 클래스이기 때문에 각종 메소드를 제공해준다.
hello/hellospring/repository/MemoryMemberRepository.java
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.stereotype.Repository;
import java.util.*;
@Repository
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 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());
}
public void clearStore(){
store.clear();
}
}
repository interface의 구현체이다.
@Repository
@Component가 포함된 annotation이다.
싱글톤 클래스 빈을 생성하는 어노테이션이다.
이 어노테이션은 선언적(Declarative)인 어노테이션이다.
즉, 패키지 스캔 안에 이 어노테이션은 "이 클래스를 정의했으니 빈(자바 객체(POJO))으로 등록해줘." 라는 뜻이 된다.
우리는 db에 저장을 하지 않고 메모리에 저장을 할껀데, Map을 사용할 것이다.
실무에선 동시성 문제 때문에 공유되는 변수일때는 ConcurrnetHashMap을 사용한다고 한다.
public Member save(Member member)
파라미터로 member 객체를 받아서 그 객체에 id를 sequence를 1증가시키고 넣어주고,
store라는 Hashmap을 생성해서 실제로 메모리에 저장하는데 id와 객체를 각각넣는다.
Hashmap에다 값을 추가해주려면 일단 먼저 new로 생성해주고,
put으로 추가해준다.
삭제해주려면, remove(key) 또는 clear()해주면 된다.
멤버 객체를 반환해준다.
public Optional<Member> findById(Long id)
그냥 store에서 get을 통해 id를 받아올 수 있지만, 그렇게 되면 null일때 NPE에러가 생길 수 있다.
따라서 아까 Optional을 활용한다.
Optional.ofNullable(store.get(id))
ofNullable은 null값을 허용한다는 것이다.
장점 : if를 이용한 null값 체크를 대체할 수 있다.
public Optional<Member> findByName(String name)
public List<Member> findAll()
모든 멤버를 반환해준다.
list형태로 반환을 할껀데, new ArrayList<>(store.values());
로 store에 저장되어있는 value인 member 객체를 리스트로 바꿔서 반환한다.
Service Layer (Business Layer)
애플리케이션 비즈니스 로직 처리와 비즈니스와
관련된 도메인 모델의 적합성 검증을 한다.
트렌젝션(DB 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한
작업의 단위 또는 한꺼번에 모두 수행되어야 할 일련의 연산들을 관리한다.
결제 시스템을 예를 들어보면,
(1) 판매처에 돈보내기, (2) 판매처에서 돈 받기 이 있다고 하면,
(1)은 성공했지만 (2)가 실패를 하게되면,
작업의 실행하기 전 상태로 돌리(rollback)는것을 하는것이 트렌젝션이다.
트랜잭션 ACID Atomicity; 원자성: 트랜잭션 내의 작업들은 모두 성공 또는 모두 실패한다. Consistency; 일관성: 모든 트랜잭션은 일관성 있는 DB 상태를 유지한다. (ex: DB의 무결성 제약 조건 항상 만족) Isolation; 격리성: 동시에 실행되는 트랜잭션들은 서로 영향을 미치지 않는다. (ex: 동시에 같은 데이터 수정 X) Durability; 지속성: 트랜잭션이 성공적으로 끝나면 그 결과는 항상 기록되어야 한다. 프레젠테이션 계층과 데이터 엑세스 계층 사이를 연결하는 역할로서 두 계층이 직접적으로 통신하지 않게 한다. |
Service 인터페이스와 @Service 어노테이션을 사용하여 작성된 Service 구현 클래스가 이 계층에 속한다.
나중에 Controller 가 Service를 통해 회원가입과 데이터를 가져올 수 있게 된다.
(컨트롤러가 서비스를 의존하는 관계)
CODE
hello/hellospring/service/MemberService.java
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@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();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
public List<Member> findMember() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
@Service
@Component가 포함된 annotation이다.
싱글톤 클래스 빈을 생성하는 어노테이션이다.
이 어노테이션은 선언적(Declarative)인 어노테이션이다.
즉, 패키지 스캔 안에 이 어노테이션은 "이 클래스를 정의했으니 빈(자바 객체(POJO))으로 등록해줘." 라는 뜻이 된다.
private final MemberRepository memberRepository;
저장할 멤버 공간을 선언한다.
@Autowired
@Autowired 개념을 알기 위해 먼저 DI(Dependency Injection) 개념을 알아야 한다.
의존대상 B가 변하면, 그것이 A에 영향을 미친다.
객체가 의존하는 또 다른 객체를 외부에서 선언하고 이를 주입받아 사용하는 것이다.
이 코드에서는 Controller 이랑 Service를 연결한다. Controller가 Service를 의존하는 관계(DI)
필요한 의존 객체의 “타입"에 해당하는 빈을 찾아 주입한다.
생성자, setter, 필드 3가지의 경우에 Autowired를 사용할 수 있다.
여기서 사용한 방법은 Constructor Dependency Injection 이다.
장점
필수적으로 사용해야 하는 레퍼런스 없이는 인스턴스를 만들지 못하도록 강제함
Spring 4.3 이상부터는 생성자가 하나인 경우 @Autowired를 사용하지 않아도 됨
Circular Dependency / 순환 참조2 의존성을 알아 차릴 수 있음
생성자에 점차 많은 의존성이 추가 될 경우 리팩토링 시점을 감지 할 수 있음
의존성 주입 대상 필드를 final로 불편 객체 선언할 수 있음
테스트 코드 작성시 생성자를 통해 의존성 주입이 용이함
단점
어쩔 수 없는 순환 참조는 생성자 주입으로 해결하기 어려움
이러한 경우에는 나머지 주입 방법 중에 하나를 사용
가급적이면 순환 참조가 발생하지 않도록 하는 것이 더 중요
public MemberService(MemberRepository memberRepository)
test에서 사용되는 repository객체와 같은 객체를 사용하기 위해서, 생성자 단에서 초기화를 시켜준다.
public Long join(Member member)
멤버를 회원가입 시키는 함수이다.
validateDuplicateMember(member);를 통해 멤버가 존재하는지 확인
문제 없으면 바로 memberRepository의 save함수를 통해 member 객체를 넘겨서 저장함.
멤버 아이디를 반환.
validateDuplicateMember(member)
멤버가 존재하는지 확인하고, 존재하면 예외를 던지는 함수이다.
memberRepository의 findByName의 멤버함수를 통해 파라미터로 들어온 멤버 객체의 이름을 확인해본다.
findByName의 리턴값이 optional 객체로 싸져있기 때문에 ifPresent를 사용할 수 있다.
ifPresent는 optional객체가 비어있으면 실행하지 않고 들어있으면, 실행한다.
따라서 만약 멤버가 존재하면 findByName에서 객체를 반환할것이고,
ifPresent에선 람다함수를 실행할 것이다. 반대로 없으면 빈 객체를 보낼것이고 람다함수를 실행하지 않을것이다.
[RuntimeException] IllegalStateException
메소드가 요구된 처리를 하기에 적합한 상태에 있지 않을때
예외를 강제로 시키려면 throw를 사용
throw new 강제시킬예외
public List<Member> findMember()
모든 멤버를 리턴해준다.
memberRepository의 멤버 findAll의 리턴값이 List이기 때문에 마찬가지로 findMemeber도 같게 해준다.
public Optional<Member> findOne(Long memberId)
한 멤버를 멤버 아이디로 찾는다.
findById도 마찬가지로 리턴값을 같게 해준다.
Presentation Layer (Controller)
- Presentation Layer는 브라우저상의 웹 클라이언트의 요청 및 응답을 처리하는 레이어이다.
- 서비스계층, 데이터 엑세스 계층에서 발생하는 Exception을 처리해준다.
- @Controller annotation을 사용하여 작성된 Controller 클래스가 이 계층에 속한다.
- 위 그림은 MVC 패턴으로 컨트롤러를 사용할때를 표시한것이지만, Controller 클래스안에 @RequestBody를 사용해서 REST API로도 만들질 수 있다.
CODE
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;
}
}
@Controller
@Component가 포함된 annotation이다.
싱글톤 클래스 빈을 생성하는 어노테이션이다.
이 어노테이션은 선언적(Declarative)인 어노테이션이다.
즉, 패키지 스캔 안에 이 어노테이션은 "이 클래스를 정의했으니 빈(자바 객체(POJO))으로 등록해줘." 라는 뜻이 된다.
private final MemberService memberService;
멤버 서비스 객체를 final로 선언.
@Autowired
이 코드에서는 Controller 이랑 Service를 연결한다. Controller가 Service를 의존하는 관계(DI)
Domain Model (Domain Object)
DB의 테이블과 매칭될 클래스
Entity 클래스 또는 가장 Core한 클래스라고 부른다.
Domain 로직만을 가지고 있어야하며 Presentation Logic을 가지고 있어서는 안된다.
DTO(Data Tranfer Object)
각 계층간 데이터 교환을 위한 객체 (데이터를 주고 받을 포맷 / 구조체)
일반적인 DTO는 로직을 갖고 있지 않다.
순수한 데이터 객체
Domain, VO(Value Object)라고도 부름
DB에서 데이터를 얻어 Service, Controller 등으로 보낼 때 사용함
로직을 갖지 않고 순수하게 getter, setter 메소드를 가진다.
Domain 클래스와 DTO 클래스를 분리하는 이유
- View Layer와 DB Layer의 역할을 철저하게 분리하기 위해서
- 테이블과 매핑되는 Entity(Domain) 클래스가 변경되면 여러 클래스에 영향을 끼치게 되지만 View와 통신하는 DTO 클래스는 자주 변경되므로 분리해야 한다.
- 즉 DTO는 Domain Model을 복사한 형태로, 다양한 Presentation Logic을 추가한 정도로 사용하며 Domain Model 객체는 Persistent(영속성(영구적으로 저장))만을 위해서 사용한다.
Dependency Injection(DI) 대해서
DI를 하는 방법이 2가지가 있다.
하나는 Component Scan이 있다. 아까 했던 방법이다. @Controller @Service @Repository annotation을 통해 Spring 빈에 등록을 한다. 여기에 이 3가지가 @Component가
you should always put most of the business logic into value objects. Entities in this situation would act as wrappers upon them and represent more high-level functionality.
출처
참고
https://yadon079.github.io/2021/spring/spring-web-layer
https://yeonyeon.tistory.com/223
https://velog.io/@gentledot/ddd-domain-model
https://multifrontgarden.tistory.com/182?category=471239
https://okky.kr/articles/779150
https://mangkyu.tistory.com/70
https://engkimbs.tistory.com/646
https://futurecreator.github.io/2018/08/26/java-8-streams/
https://codechacha.com/ko/stream-filter/
'Java Spring' 카테고리의 다른 글
Server-Sent Events vs WebSockets (0) | 2024.07.02 |
---|---|
Spring CMD 환경에서 빌드하기 (0) | 2024.03.04 |
Spring 라이브러리 ( 김영한 스프링 입문 ) 강의중.. (0) | 2024.03.04 |
Spring 프로젝트 생성 및 시작하기 ( IntelliJ & Spring initializr ) (0) | 2024.03.04 |
Java Spring 의존성 추가 방법 ( IntelliJ) (0) | 2024.03.04 |