해당 글은 DEPth IT 연합 프로젝트 동아리 활동의 일환인 서버 스터디에 관련되어 있습니다.🐧
웹 어플리케이션 계층 구조
해당 사진은 일반적인 웹 어플리케이션 계층 구조입니다. 각 계층에 대해서 설명하겠습니다.
컨트롤러
컨트롤러는 클라이언트의 요청을 받아서 해당 요청에 맞는 작업을 수행합니다. 그리고 그 결과를 다시 클라이언트에게 반환합니다. 사용자의 액션을 처리하고 그 결과를 사용자에게 다시 반환합니다. 주로 라우팅과 비즈니스 로직의 흐름을 관리합니다.
서비스
컨트롤러가 요청을 받으면 해당 부분에서 비즈니스 로직을 수행합니다. 실질적인 작업을 수행하는 곳이라고 볼 수 있습니다. 여러 컨트롤러가 공통으로 사용하는 기능들이나 복잡한 로직들을 처리합니다. 주로 도메인 로직을 구성하고 데이터 조작을 담당합니다.
레포지토리
데이터베이스와의 상호 작용을 담당합니다. 데이터베이스와의 통신, 테이터의 Creat, Read, Update, Delete를 담당합니다. 실제로 서비스나 컨트롤러에서 필요한 데이터를 가져와 저장하는 역할을 합니다.
도메인
어플리케이션의 핵심 부분으로, 실제 업무 영역의 모델과 관련된 부분입니다. 어플리케이션의 핵심 개념이나 데이터 구조를 나타내며, 비즈니스 규칙을 캡슐화합니다. 데이터베이스와는 독립적으로 설계되며, 비즈니스 요구사항을 반영합니다.
DB
실제 데이터를 저장하고 관리하는 공간입니다. 레포지토리와의 상호작용을 통해서 테이터를 읽고 쓰게 됩니다. 관계현 데이터베이스 혹은 NoSQL 데이터베이스 일 수 있습니다.
클래스 의존 관계
클래스를 설계하면 위와 같은 상황이며, 의존 관계를 갖습니다.
Member Service에는 회원 비즈니스 로직이 존재합니다.
Member Repository는 인터페이스로 설계합니다. 해당 인터페이스에는 정보 저장을 위한 메서드가 존재합니다. 정보 저장과 관련된 메서드를 인터페이스로 구현하는 이유는 해당 강의의 설정 과정에서 정보 저장소를 아직 결정하지 못하였기 때문입니다. 따라서 정보 저장소가 결정되더라도 유연성 있게 대처하기 위해서 인터페이스로 구현을 하였습니다.
Memory Member Repository에서는 임시적으로 개발을 진행하기 위해서 메모리 기반의 데이터 저장소를 사용하는 구현체를 만듭니다.
회원 도메인 만들기
위에서 언급한 웹 어플리케이션의 계층 구조에 알맞게 실제로 코드를 작성합니다. 회원 도메인을 만들기 위해서 도메인 패키지를 만듭니다. 이후 해당 패키지 안에 Member 클래스를 만듭니다. 아래는 Member 클래스 코드입니다.
package hello.hellospring.domain;
public class Member {
private Long id; //데이터를 구분해주기 위해서 저장되는 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 값은 데이터를 구분하기 위해서 저장되는 id 값입니다. name은 회원의 이름을 나타냅니다. 이후 Getter and Setter를 구현합니다.
레포지토리 만들기
위에서 언급한 웹 어플리케이션의 계층 구조에 알맞게 실제로 코드를 작성합니다. 레포지토리 패키지를 만듭니다. 이후 인터페이스와 해당 인터페이스를 사용하는 클래스를 만듭니다. 아래는 순서대로 인터페이스와 클래스입니다.
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(); //지금까지 저장된 모든 회원 리스트의 반환.
}
위에서 만든 domain 패키지의 Member 클래스를 import한 모습을 볼 수 있습니다.
Member를 사용해서 4개의 메서드를 코딩합니다. 각각의 메서드의 목적은 주석에서 확인 할 수 있습니다. 이때 정보를 찾아오는 함수는 null값을 반환할 수 있기에 Optional을 사용합니다.
⚙️ Optional?
자바 8부터 소개된 클래스로, 값이 존재할 수도 있고 아닐수도 있는 컨테이너 객체를 의미합니다. 해당 클래스는 null 대신 사용되어야 하는 값의 존재 여부를 명시적으로 표현하고, 이를 통해서 NullPointerException등의 예외를 방지하고 코드의 가독성을 방지 시킵니다. null을 사용하는 것보다 값이 없는 상황을 명시적으로 다룰수 있어서 안전성을 높일 수 있습니다.
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 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());
}
}
위의 인터페이스를 사용하는 클래스를 만들어 줍니다. 위의 클래스에는 간단한 예제로써 만들어졌기에 동시성 문제를 신경쓰지 않고 Map, Long을 사용하였습니다.
Save 함수에서는 구분이 가능하도록 sequence를 ++하여서 id값을 저장합니다. 이후 map에 id 값을 key로 사용하여서 member를 저장합니다.
findById에서는 ofNullable을 이용해서 Optional 값을 가져옵니다. 값이 있던 없던 optional로 감싸서 NullPointerException을 방지하고 안전하게 데이터를 처리 할 수 있게 합니다.
findByName은 store에서 저장된 멤버 객체의 컬렉션을 가져옵니다. 이후 stream을 생성하여서 모든 객체를 탐색합니다. 탐색하며 주어진 이름과 member의 이름이 같은 것만 필터링합니다. 이후 값이 있건 없건 optional에 넣어서 처리합니다.
findAll은 store에 저장된 멤버 객체의 컬렉션을 Array로 반환합니다.
⚙️ 동시성 문제?
프로그램이 여러 스레드에 의해 동시에 실행될 때 발생할 수 있는 문제를 가리킵니다. 다수의 스레드가 동일한 자원에 접근하거나 수정할때, 그 자원의 상태가 예기치 않게 변경될 수 있기에 발생하는 문제입니다. 해당 문제는 잘못 다루어지는 경우 어플리케이션의 안정성과 성능에 영향을 미칠수 있습니다. 해당 문제의 해결을 위해서는 공유되는 자원에 대한 동기화 메커니즘을 이용해서 접근을 제어하는 방법이 있습니다.