본문 바로가기
Back-End/🌱 Spring Boot (java)

[Spring 입문] 계층 구조와 클래스 의존관계/주입 (Dependency Injection) 실습

by 코딩하는 동현😎 2023. 1. 15.

입문자의 비즈니스 실습 요구 사항 정리

  • 데이터 : 회원 ID , 이름
  • 기능 : 회원 등록, 조회
  • 아직 데이터저장소가 없다고 가정하고, 자바내부 자료구조를 이용해서 모의로 실습 해볼 것입니다.

서비스, 리포지토리와 , 도메인관계

웹앱의 구조는 보통 위와 같이 분류가 됩니다.

  • 컨트롤러 : 웹MVC의 컨트롤러 역할로 받는 라우터 마다 나눠서 정의 합니다.
  • 서비스 : 핵심 기능의 로직을 구현합니다. (컨트롤러에서 서비스의 함수를 호출하는 방식)
  • 도메인 : '흔히 아는 객체'. 회원,주문,쿠폰 등등의 객체를 클래스로 정의합니다.
  • 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장, 조회, 관리 등등을 합니다.

클래스 의존관계/주입 (Dependency Injection)

어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나 해당 클래스의 객체의 메소드를 호출하는 경우

두 클래스에 사이에 의존관계가 있다고 말합니다.

레포지토리는 도메인을 저장,조회하는 기능을 수행하므로, 도메일의 클래스를 호출해서 사용하는 것인데, 이것도 DI로 볼수 있습니다.

자바 인터페이스란 기능에 대한 명세 집합으로 자바를 공부한 사람들은 다 아므로 깊게 다루지 않겠습니다. 공통적인 함수들을 추상적으로 정의하고 실질적 기능을 하는 클래스가 implements를 이용해서 상속 받아서 작성합니다.

MemberRepository라는 추상적인 인터페이스를 만들고, MemoryMemberRepository에 실질적으로 데이터를 다뤄보겠습니다.

컨트롤러 부분은 저번에 학습했고, 간단하므로 다음에 하겠습니다. 지금은 서비스,도메인,리포지토리 만들고 나중에 테스트 케이스로 검증해보겠습니다.

의존관계는 실제 실습 코드를 보면 이해가 될 것입니다.


도메인 멤버객체 정의

domain 패키지를 만들고 안에 Member클래스를 넣습니다.

패키지는 간단하게 폴더를 만들고 안에 있는 클래스에 package <폴더경로>;라고 선언하면 완성됩니다. (이것도 보통 자동완성으로 처리될 것입니다.)

 

domain/Member.java

package example.boot.domain;

public class Member {
    private Long id;
    private String name;

    public Long getId(){
        return this.id;
    }

    public void setId(Long id){
        this.id = id;
    }

    public String getName(){
        return this.name;
    }

    public void setName(String name){
        this.name = name;
    }
}

레포지토리 - 인터페이스 정의

저장 조회등의 함수를 추상적으로 정의를 해줍니다.

나중에 상속 받으면 그 클래스에 이 함수들이 자동으로 오버라이드 할수 있도록 자동생성됩니다.

 

repository/MemberRepository.java

package example.boot.repository;

import java.util.Optional;
import java.util.List;

import example.boot.domain.Member;

public interface MemberRepository {
    // 인터페이스란 기능에 대한 명세 집합
    // 공통적인 함수들을 정의하고 실질적인 repository에 상속 받는다

    // 멤버를 받으면 저장
    Member save(Member member);

    // id / name 으로 받고 member를 반환
    Optional<Member> findById(Long id);

    Optional<Member> findByName(String name);

    //여러 멤버들을 리스트에 담아서 묶음으로 반환
    List<Member> findAll();
}

Optional이란? Optional 개념 및 사용법

NPE(NullPointerException)

개발을 할 때 가장 많이 발생하는 예외 중 하나가 바로 NPE(NullPointerException)입니다. NPE를 피하려면 null 여부를 검사해야 하는데, null 검사를 해야하는 변수가 많은 경우 코드가 복잡해지고 번거롭다.

그래서 null 대신 초기값을 사용하곤 한다.

아래 코드가 예외처리를 위한 코드입니다.

List<String> names = getNames();
names.sort(); // names가 null이라면 NPE가 발생함

List<String> names = getNames();
// NPE를 방지하기 위해 null 검사를 해야함
if(names != null){
    names.sort();
}

 

 

 

Optional

Java8에서는 Optional<T> 클래스를 사용해 NPE를 방지할 수 있도록 도와줍니다. Optional<T>는 null이 올 수 있는 값을 감싸는 Wrapper 클래스로, 참조하더라도 NPE가 발생하지 않도록 도와준다. Optional 클래스는 값이 null이더라도 바로 NPE가 발생하지 않으며, null일때 조건문을 적용하지 않아도 되도록 각종 메소드를 제공해줍니다.

현업에서 많이 쓰는 클래스 이므로 사용하게 됐고, 이 포스트에서 자세하게 다루진 않겠습니다.


인터페이스 상속 - 실질적 레포지토리 클래스

클래스 파일을 만들고 implements로 인터페이스를 상속해주고, 아래 사진과 같이 상속 클래스를 불러와줍니다.

적용시켜주면 아래와 같이 자동으로 함수들이 오버라이딩 할 수 있도록 생성됩니다.

 

코드에 대한 설명들은 주석에다가 다 작성했습니다.

 

repository/MemoryMemberRepository.java

package example.boot.repository;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ArrayList;


import example.boot.domain.Member;

public class MemoryMemberRepository implements MemberRepository {

    // 아직 데이터베이스를 연결 안했으므로 간이로 데이터베이스(store해시맵) 과 id를 일일히 할당해줍시다
    // 데이터베이스가 자동으로 id를 할당하지만 아직 연결 안했으므로 id  1씩 증가하면서 하겠습니다.
    private static Map<Long, Member> store = new HashMap<Long,Member>();
    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) {
        // Optional은 null 반환할 수도 있는데 그대로 반환 시킨다. -> 프론트에서 null 처리
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        // value들, 즉 멤버들을  stream으로 하나씩 확인하는데 filter 람다식으로 이름이 같은것만 고릅니다
        // findAny로 일치하는 것을 반환합니다.
        return store.values().stream().filter(m -> m.getName().equals(name)).findAny();
    }

    @Override
    public List<Member> findAll() {
        // 저장된 값들을 arrayList로 변환해서 반응
        return new ArrayList<Member>(store.values());
    }
    public void clearStore(){
        store.clear();
    }
    
}

멤버 서비스 클래스 

 

회원가입

서비스는 핵심 기능의 로직을 구현합니다.

기존에 있는 레포지토리를 이용해서 회원가입 탈퇴 등등을 수행하고, 이것도 레포지토리에 서비스 의존성 주입으로 볼 수 있습니다.

 

레포지토리 의존성을 이용해서 회원 가입하기 (중복된 이름이 있으면 오류를 일으킵니다.)

service/MemberService.java

package example.boot.service;

import example.boot.domain.Member;
import example.boot.repository.MemberRepository;
import example.boot.repository.MemoryMemberRepository;

public class MemberService {
    
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    // 회원 가입
    public Long join(Member member){
        // 같은 이름이 있는 중복 회원 X
        // Optional 객체 이므로 ifPresent 함수를 이용해서 null 조건문 없이 작성 할 수 있습니다.
        memberRepository.findByName(member.getName()).ifPresent(m -> {
            throw new IllegalStateException("already Exists");
        });
        memberRepository.save(member);
        return member.getId();
    }
}

가입할때 검증하는 것은 따로 함수로 많이 지정하므로 아래와 같이 더 코드를 깔끔하게 개선 할 수 있습니다.

service/MemberService.java

package example.boot.service;

import example.boot.domain.Member;
import example.boot.repository.MemberRepository;
import example.boot.repository.MemoryMemberRepository;

public class MemberService {
    
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    // 회원 가입
    public Long join(Member member){
        // 같은 이름이 있는 중복 회원 X
        validateOverapMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    // 중복된 회원이 있는지 검증해주는 함수
    public void validateOverapMember(Member member){
        // Optional 객체 이므로 ifPresent 함수를 이용해서 null 조건문 없이 작성 할 수 있습니다
        memberRepository.findByName(member.getName()).ifPresent(m -> {
            throw new IllegalStateException("already Exists");
        });
    }

}

멤버 서비스 - 의존성 주입을 통한 회원 조회 

service/MemberService.java 아래부분

    public List<Member> findAll() {
        return memberRepository.findAll();
    }

    public Optional<Member> findOne (Long id){
        return memberRepository.findById(id);
    }

이렇게 실습 코드들을 보고 다시 처음에 개념 설명을 보세요!

 

어떤 식으로 Dependency Injection이 일어나는지 이해가 될것입니다.

DI에 경우에는 스프링프레임워크의 핵심이고, 많은 NestJs등 백엔드 프레임워크에서도 적용되므로 잘 이해하시길 바랍니다!

반응형

댓글