본문 바로가기

Spring/스프링 입문

02. 회원 관리 페이지 만들기 - (2) 회원 도메인과 리포지토리 만들기

💡 본 게시글은 김영한님의 인프런(Inflearn) 강의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술에 대해 공부하고, 정리한 내용입니다.

1) 회원 도메인 만들기

 먼저, 'domain'이라는 패키지를 생성하고 그 안에 'Member' 클래스를 만들어 보겠습니다. 이 클래스는 회원 정보를 저장할 변수들을 선언합니다.

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' '이름'입니다.

 

 여기서 회원 ID는 시스템이 데이터를 구분하기 위해 지정하는 값으로, Long 타입의 변수 'id'를 선언하겠습니다. 또한 이름은 회원이 직접 설정하는 값으로, String 타입의 변수 'name'을 선언하겠습니다.

 

 이후에는 각 변수에 대한 getter와 setter를 생성합니다. (getter와 setter의 사용에 대한 의견은 다양하지만, 이 예제에는 간단히 getter와 setter를 사용하였습니다.)


2) 회원 리파지트리 인터페이스 만들기

먼저 'repository'라는 패키지를 생성합니다. 'Repository'는 저장소라는 뜻으로, 이 예제에서는 회원 객체를 저장할 저장소를 만들 예정입니다.

 

다음으로 'MemberRepository'라는 인터페이스를 만듭니다.

public interface MemberRepository {

    Member save(Member member); // 1. 저장
    Optional<Member> findById(Long id); // 2. 시스템이 인식하는 ID를 통해서 member 찾기
    Optional<Member> findByName(String name); // 3. 사용자 이름을 통해 member 찾기
    List<Member> findAll(); // 4. 저장된 모든 회원 리스트 반환
}

 

위의 인터페이스는 다음의 기능들을 정의하고 있습니다:

 

(1) save

  • member에 들어갈 정보(id, name)을 받아 이를 저장하는 기능

(2) findById

  • 시스템이 인식하는 id를 받아 해당 id를 갖고 있는 member를 찾는 기능

(3) findByName

  • 사용자의 이름을 받아 해당 이름을 갖고 있는 member를 찾는 기능

(4) findAll

  • 지금까지 저장된 모든 회원 리스트를 반환하는 기능

※ 인터페이스의 장점: 

 인터페이스는 함수의 구현부가 없으며, 추상 메서드와 상수만을 가질 수 있습니다. 또한, 인터페이스를 상속받는 클래스에게 함수 구현을 강제하여 반드시 구현해야 하는 기능을 구현할 수 있도록 돕습니다.

 

=> 여러 클래스가 해당 인터페이스를 상속받았을 때, 인터페이스만을 수정하여 개발 코드의 수정을 줄여 유지보수성을 높일 수 있습니다.

 

※ 인터페이스로 개발하는 이유: 

 만약 메모리 기반 데이터 저장소 클래스에 모든 내용을 구현하는 것도 나쁘지 않겠지만, 데이터 저장소를 선정하고 해당 데이터 저장소로 기능들을 옮기려면 메모리 기반 데이터 저장소의 코드를 모두 직접 옮겨야 할 것입니다. 하지만 interface로 필요한 기능들을 선언하고, 이를 상속받아 사용한다면 저장소 교체가 더욱 수월할 것입니다.

 

Optional 자료형

 'Optional'은 Java 8에서 도입된 기능으로, null을 직접 다루는 것보다 안전하고 편리하게 객체의 존재 또는 부재를 표현할 수 있도록 도와줍니다. 이는 null 포인터 예외를 방지하고 코드의 가독성을 높이는 데 도움을 줍니다.

 

 Optional 객체어떤 타입의 값이 존재할 수도, 존재하지 않을 수도 있는 상황을 표현하며, 이를 통해 개발자는 명시적으로 해당 값의 존재 여부를 체크할 수 있게 됩니다.

 

예를 들어, 아래와 같이 사용할 수 있습니다.

Optional<String> optional = Optional.of("Hello, World!");

if (optional.isPresent()) {
    System.out.println(optional.get());
} else {
    System.out.println("Value is absent");
}

 

 위의 코드에서 `Optional.of` 함수를 통해 "Hello, World!"라는 값을 갖는 Optional 객체를 생성했습니다. 그리고 `isPresent` 메서드를 통해 값이 존재하는지 확인하고, `get` 메서드를 통해 값을 가져옵니다.

 

 만약 값이 존재하지 않을 경우, `get` 메서드는 `NoSuchElementException`을 발생시키므로, 반드시 `isPresent` 메서드로 확인한 후에 값을 가져와야 합니다. Optional 메서드 이외에도, `orElse`, `orElseGet`, `orElseThrow` 등 다양한 메서드를 제공하여 값의 존재 여부에 따른 다양한 동작을 수행할 수 있습니다.


3) 구현체 만들기

interface 제작이 완료되면 이제 '구현체 생성'을 합니다.

 

 우선 'repository' 패키지 내에 'MemoryMemberRepository' 클래스를 생성하고, 이를 통해 앞서 선언한 인터페이스를 상속을 받습니다.

public class MemoryMemberRepository implements MemberRepository{

    @Override
    public Member save(Member member) { }

    @Override
    public Optional<Member> findById(Long id) { }

    @Override
    public Optional<Member> findByName(String name) { }

    @Override
    public List<Member> findAll() { }
}

 이 상태에서는 인터페이스에서 선언한 메서드들을 오버라이드하지 않았기 때문에 오류가 발생합니다. 이를 해결하기 위해 'Alt+Enter'를 누르고 'Implements methods'를 선택하면 위와 같은 상태가 됩니다.

 

메서드들을 구현하기 전에, 필요한 멤버 변수들을 선언합니다.

private static Map<Long, Member> store = new HashMap<>(); // member 객체 저장
private static long sequence = 0L; // 시스템이 부여하는 ID 번호

 

 회원 정보를 저장할 때 필요한 저장 공간이 필요합니다. 메모리 기반 저장소를 사용할 예정이므로, 이번 예제에서는 Map을 사용합니다.

 

 또한, 시스템이 인식하는 id가 있는데, 회원의 정보를 저장할 때 id를 부여해야 합니다. 그러므로 long형 변수를 선언하고, 새로운 멤버 객체를 저장할 때마다 값을 하나씩 증가시켜서 id를 부여합니다.

 

이제 오버라이드된 메서드들을 구현해보겠습니다.


(1) Save

 이 'save' 메서드는 Map에 member 객체를 저장하고, 저장한 객체를 반환하는 역할을 합니다.

@Override
public Member save(Member member) {
    member.setId(++sequence);
    store.put(member.getId(), member);
    return member;
}

member 객체의 변수에는 'id'와 'name'이 있습니다. 여기서 'name'은 사용자로부터 이미 입력을 받은 상태라고 가정하겠습니다.

 

이 메서드는 다음과 같은 순서로 동작합니다:

  1. '++sequence'를 통해 id 값을 증가시킵니다.
  2. 'setId' 메서드를 통해 해당 멤버의 id를 설정합니다.
  3. 현재 member의 id와 member 객체를 Map에 쌍으로 저장합니다.
  4. 저장한 member 객체를 반환합니다.

(2) findById

이 메서드는 시스템이 구별하는 id를 통해 member 객체를 찾는 역할을 합니다.

@Override
public Optional<Member> findById(Long id) {
    return Optional.ofNullable(store.get(id));
}

 

 여기서 주목해야 할 것은 'Optional'입니다. HashMap에서 id(key)에 해당하는 member 객체(value)를 반환하는데, 해당 id가 없을 수도 있습니다.

 

'Optional'은 객체를 포장하는 Wrapper Class로, null이 될 가능성이 있는 경우에 사용하여 Null Pointer Exception(NPE)을 방지할 수 있습니다.

 

즉, 'Optional.ofNullable'로 객체를 감싸주면, 해당 객체가 null일 경우 빈 Optional 객체를 반환하여 NPE를 방지합니다.

 

이렇게 Optional로 감싸서 반환해주면, 클라이언트에서 이를 처리할 수 있습니다. 이에 대한 자세한 설명은 이어질 내용에서 다루도록 하겠습니다.


(3) findByName

이 메서드는 문자열 'name'을 받아 HashMap에서 해당 이름을 가진 member 객체를 반환하는 기능을 합니다.

@Override
public Optional<Member> findByName(String name) {
    return store.values().stream()
        .filter(member -> member.getName().equals(name))
        .findAny();
}
 'Java 8'부터 지원하는 'stream'을 이용해 구현해 보겠습니다. 'stream'은 배열 또는 컬렉션 인스턴스를 효과적으로 다룰 수 있는 방식입니다.
 
'store.values().stream()'은 store(HashMap)의 value들, 즉 member 객체들을 순회하면서 처리한다는 의미입니다.
 
'.filter(member -> member.getName().equals(name))'은 member 객체의 'getName'이 파라미터로 받은 'name'과 동일한지 확인하고, 동일한 경우에만 필터링을 합니다.
 
'.findAny()'필터링된 결과 중 하나라도 있다면, 그 결과를 Optional로 반환합니다. 만약 값이 없다면 null이 포함된 Optional이 반환됩니다.

(4) findAll

이 메서드는 지금까지 저장한 모든 member 객체들을 List 형태로 반환하는 메서드입니다.

@Override
public List<Member> findAll() {
    return new ArrayList<>(store.values());
}

여기서 주목할 점은 member 객체들이 id라는 key와 연결되어 HashMap에 저장되었음에도 불구하고, 반환 값은 List라는 점입니다. 따라서 새로운 ArrayList를 생성하고, 그 안에 'store.values()', 즉 HashMap의 모든 value들(즉, member 객체들)을 담아 반환합니다.