Spring Example: ToDo List #2 엔티티, 리포지토리, 서비스 개발

함형주·2022년 9월 17일
0

Spring Example: ToDo

목록 보기
3/16

질문, 피드백 등 모든 댓글 환영합니다!

지난 블로그에 이어서 entity -> repository -> service 순으로 개발하고 테스트 코드를 작성합니다.
기능별로 git branch를 추가하고 main branch에 merge 해주는 방식으로 개발합니다.

Entity 개발

domain 패키지 안에 Member와 ToDo 클래스를 생성하고 핵심 필드 개발 후 Jpa관련 로직을 추가하겠습니다.

Member

public class Member {

    private Long id;

    private String loginId;
    private String password;
    private String name;

    public Member(String loginId, String password, String name) {
        this.loginId = loginId;
        this.password = password;
        this.name = name;
    }
}

Member 객체는 변수로 식별자인 id와 loginId, password, name을 가짐.

ToDo

public class ToDo {

    private Long id;

    private String title;
    private String description;

    private Boolean isCompleted = false;

    private LocalDate createdDate;
    private LocalDate dueDate;

    public ToDo (String title, String description, LocalDate dueDate) {
        this.title = title;
        this.description = description;
        this.createdDate = LocalDate.now();
        this.dueDate = dueDate;
    }

    public void changeStatus() {
        this.isCompleted = !this.isCompleted;
    }
}

ToDo 객체는 변수로 식별자인 id와 title, description, isCompleted(defalt = false),
createDate(객체 생성 시점에 값 할당), dueDate를 가짐.

JPA 로직 추가

사용한 공통 annotation

@Entity : 테이블과 1:1로 매칭되는 객체
@Id : Primary key로 지정
@GernaratedVaule : Pk값 자동 생성, default - 키 생성 방식을 DB에 위임
@Column : 테이블의 column과 1:1 매칭, name 속성으로 column 이름 지정 가능

@Getter : Lombok이 제공하는 기능. 모든 필드에 getXXX 메서드 제공
@NoArgsConstructor : Lombok이 제공하는 기능. 기본 생성자 자동 생성, entity는 기본 생성자가 필수
- (access = AccessLevel.PROTECTED) : 접근 제한자를 protected로 설정 (스프링에선 관례상 생성자를 protected로 설정하면 이 생성자는 사용하지 말라는 의미를 가짐)

Member

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

    @Id @GeneratedValue @Column(name = "member_id")
    private Long id;
    .
    .
    
    @OneToMany(mappedBy = "member")
    private List<ToDo> toDoList = new ArrayList<>();
    .
    .
 }

@OneToMany : 1:N 연관관계 매핑, N쪽이 FK를 가진 연관관계의 주인이므로 mappedBy로 연관관계 지정. ("member"는 @ManyToOne이 지정된 변수값)

ToDo

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ToDo {

    @Id @GeneratedValue @Column(name = "todo_id")
    private Long id;
    .
    .
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
    
        public static ToDo createToDo(String title, String description, LocalDate dueDate, Member member) {
        ToDo toDo = new ToDo();
        toDo.title = title;
        toDo.description = description;
        toDo.createdDate = LocalDate.now();
        toDo.dueDate = dueDate;
        if (member != null) toDo.configMember(member);

        return toDo;
    }
    
        private void configMember(Member member) {
        this.member = member;
        member.getToDoList().add(this);
    }
}

@ManyToOne : N:1 연관관계 매핑, fetch = Lazy는 select 쿼리를 호출할 시 연관된 객체는 값이 직접적으로 접근될 때 select 쿼리가 다시 호출 됨 (쿼리 성능 최적화)
@JoinColumn : 연관관계 매핑 시 외래키 지정

생성자에 추가적인 로직이 포함되었으므로 static 메서드로 바꿔주었음.
configMember()로 ToDo 생성 시 Member 객체와의 연관관계도 함께 지정.

Repository 개발

Spring Data Jpa 사용

public interface MemberRepository extends JpaRepository<Member, Long> {

    Optional<Member> findByLoginId(String loginId);
}
public interface ToDoRepository extends JpaRepository<ToDo, Long> {

}

ToDo List #0에서 Spring data jpa를 사용했기 때문에 JpaRepository를 상속받아 인터페이스를 만들면 기본적인 CRUD 메서드를 자동으로 생성하고 심지어 인터페이스의 구현체마저 생성해줍니다. (@Repository 포함)

우선 여기까지 개발하고 후에 메서드를 추가할 일이 있으면 그 때 추가해주겠습니다.

Service 개발

Service 계층은 객체지향 5원칙 중 DIP(의존성 역전의 원칙)를 지키기 위해 일부러 Service 인터페이스와 그 구현체를 따로 개발했습니다.
별다른 확장이나 수정 사항이 잘 떠오르지 않을 정도로 작고 간단한 프로젝트지만 Spring 을 공부하는 만큼 객체지향 원칙을 지키며 개발하려고 노력했습니다.

사용한 공통 annotation

@Service : 스프링이 Service 계층으로 인식, 컴포넌트 등록(@Component 내장)
@Transactional : 트랜잭션의 생명주기. 모든 Jpa 로직은 트랜잭션 안에서 이루어져야 함. readOnly = true 로 설정하면 읽기 전용
@RequiredArgsConstructor : private와 final 이 붙은 필드의 생성자를 만들고 스프링 빈에 등록된 객체 중에서 적절한 객체를 자동으로 주입해 줌.

MemberService

public interface MemberService {

    Long save(Member member);

    List<Member> findAll();

    Member findById(Long id);
}

MemberServiceImpl

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService{

    private final MemberRepository repository;

    @Override
    @Transactional
    public Long save(Member member) {
        // loginId 중복 체크
        return repository.findByLoginId(member.getLoginId()).isEmpty() ? repository.save(member).getId() : null;
    }

    @Override
    public List<Member> findAll() {
        return repository.findAll();
    }

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

추후 필요시 기능 확장 예정.

ToDoService

public interface ToDoService {

    Long save(ToDo toDo);

    ToDo findById(Long id);

    List<ToDo> findAll();

    void delete(ToDo toDo);

}

ToDoServiceImpl

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ToDoServiceImpl implements ToDoService{

    private final ToDoRepository repository;

    @Override
    @Transactional
    public Long save(ToDo toDo) {
        return repository.save(toDo).getId();
    }

    @Override
    public Optional<ToDo> findById(Long id) {
        return repository.findById(id);
    }

    @Override
    public List<ToDo> findAll() {
        return repository.findAll();
    }

    @Override
    @Transactional
    public void delete(ToDo toDo) {
        repository.delete(toDo);
    }
}

추후 필요시 기능 확장 예정.

LoginService

public interface LoginService {

    Optional<Member> login(String loginId, String password);
}

LoginServiceImpl

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class LoginServiceImpl implements LoginService{

    private final MemberRepository repository;

    @Override
    public Optional<Member> login(String loginId, String password) {
        Optional<Member> findMember = repository.findByLoginId(loginId);
        return findMember.filter(member -> member.getPassword().equals(password));
    }
}

Optional.filter()는 Optional의 객체가 존재하며(!null) filter 안의 람다식이 true 일 경우 Optional을 그대로 반환, false면 빈 Optional을 반환합니다. 이 블로그를 참고했습니다.

다음으로

사실 이미 테스트 코드까지 작성했지만 코드가 많아 블로그가 너무 길어지는 관계로 나머지는 다음 블로그에 작성하겠습니다.
다음 블로그에는 테스트 코드 작성과 ToDo의 수정로직 추가, 기타 갖가지 코드 수정사항 등이 포함될 예정입니다.


github , 배포 URL (첫 접속 시 로딩이 걸릴 수 있습니다.)

profile
평범한 대학생의 공부 일기?

0개의 댓글