Project/Todo

[Spring Boot] Todo 서비스 구현 (1)

lakelight 2022. 7. 17. 16:05
728x90
반응형

프로젝트 구성

프로젝트 구성

 

도메인을 생성할 때 클래스는 한가지 기능만을 하도록 하는 SRP를 지켰고,
관심사를 분리하여 DIP를 지키기 위해 생각을 하며 코딩했습니다.
SRP, DIP에 대한 자세한 설명
 

[Spring] 객체 지향 설계의 다섯 가지 기본 원칙 - SOLID

SOLID 다섯 가지 설계 원칙 단일 책임 원칙(SRP: Single Responsibility Principle) 한 클래스는 하나의 책임만 가져야 한다. 책임 영역이 확실해지고, 한 클래스의 변경이 다른 클래스의 영향을 미치지 않습

lakelight.tistory.com

 

[Todo.java]

package hooyn.todo.domain;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.LocalDateTime;

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

    @Id @GeneratedValue
    @Column(name = "todo_id")
    private Long id;

    private String title;
    private String content;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "date", column = @Column(name = "deadline_date")),
            @AttributeOverride(name = "time", column = @Column(name = "deadline_time"))
    })
    private Deadline deadline;

    private LocalDateTime create_time;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "uuid")
    private Member member;

    //연간관계 편의 메서드
    void setMember(Member member){
        this.member = member;
        member.getTodos().add(this);
    }

    //변경감지를 위한 set 메서드
    public void setTitle(String title){
        this.title = title;
    }

    public void setDeadline(Deadline deadline){
        this.deadline = deadline;
    }

    public void setContent(String content) {
        this.content = content;
    }

    //생성 매서드

}

 

투두 기한을 위한 클래스(Deadline)를 생성하여 SRP를 지켰습니다.
그리고 데이베이스에 저장될 column name도 @AttributeOverrides를 통해  지정해주었습니다.

또한 Member도 다대일 매핑관계를 추가해주었고 엔티티 조회 시 Member는 직접 접근(Get메서드)을
하지 않으면 프록시 객체로 가져와 Member 엔티티를 가져오지 않게 LAZY로 설정하였습니다. 

그리고 Todo를 저장할 때 Member와의 연관관계를 설정해주기 위해
연관관계 편의 메서드도 생성해주었습니다.

 

[Update Todo.java]

package hooyn.todo.domain;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.LocalDateTime;

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

    @Id @GeneratedValue
    @Column(name = "todo_id")
    private Long id;

    private String title;
    private String content;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "date", column = @Column(name = "deadline_date")),
            @AttributeOverride(name = "time", column = @Column(name = "deadline_time"))
    })
    private Deadline deadline;

    private LocalDateTime create_time;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "uuid")
    private Member member;

    //연간관계 편의 메서드
    private void setMember(Member member){
        this.member = member;
        member.getTodos().add(this);
    }

    //변경감지를 위한 set 메서드
    public void changeTitle(String title){
        this.title = title;
    }

    public void changeDeadline(Deadline deadline){
        this.deadline = deadline;
    }

    public void changeContent(String content) {
        this.content = content;
    }

    //빌더를 통한 생성 매서드
    public static Todo createTodo(String title, String content, Deadline deadline, Member member){
        Todo todo = Todo.builder()
                .title(title)
                .content(content)
                .deadline(deadline)
                .build();

        todo.setMember(member);

        return todo;
    }

    @Builder
    private Todo(String title, String content, Deadline deadline){
        this.title = title;
        this.content = content;
        this.deadline = deadline;
        this.create_time = LocalDateTime.now();
    }
}

 

변경감지를 위한 메서드의 이름을 Set보다Change로 하는 것이 좋다고 하여 수정하였습니다.
또한 투두의 무분별한 생성을 막기위해서 빌더를 통해 생성메서드를 구현하였습니다.

빌더를 사용하는 이유
1. 필요한 데이터만 설정할 수 있습니다.
2. 유연성을 확보할 수 있습니다.
3. 가독성을 높일 수 있습니다.
4. 불변성을 확보할 수 있습니다.

 

[Deadline.java]

package hooyn.todo.domain;

import lombok.Getter;

import javax.persistence.Embeddable;

@Embeddable
@Getter
public class Deadline {
    private String date;
    private String time;
}

 

[TodoRepository.java]

package hooyn.todo.repository;

import hooyn.todo.domain.Deadline;
import hooyn.todo.domain.Todo;

import java.util.List;

public interface TodoRepository {

    //투두 작성
    Long save(Todo todo);

    //투두 엔티티 검색
    Todo findById(Long id);

    //투두 UUID에 따른 엔티티 검색
    List<Todo> findByUUID(String uuid);

    //투두 Deadline에 따른 엔티티 검색 + 페이징 변수 추가
    List<Todo> findByDeadline(String uuid, Deadline deadLine, Integer page);

    //투두 콘텐츠 키워드에 따른 엔티티 검색 + 페이징 변수 추가
    List<Todo> findByContent(String uuid, String content, Integer page);

    //투두 수정, 삭제 권한 확인
    boolean checkAuthorization(String uuid, Long id);

    //투두 삭제
    Long delete(Long id);
}

 

[TodoRepositoryImpl.java]

package hooyn.todo.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import hooyn.todo.domain.Deadline;
import hooyn.todo.domain.QTodo;
import hooyn.todo.domain.Todo;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.UUID;

import static hooyn.todo.domain.QTodo.todo;

@Repository
@RequiredArgsConstructor
@Primary
public class TodoRepositoryImpl implements TodoRepository {

    private final EntityManager em;
    private final JPAQueryFactory queryFactory;

    /**
     * 투두 작성
     */
    @Override
    public Long save(Todo todo) {
        em.persist(todo);

        return todo.getId();
    }

    /**
     * 투두_아이디에 따른 투두 조회
     */
    @Override
    public Todo findById(Long id) {
        return em.find(Todo.class, id);
    }

    /**
     * UUID에 따른 투두 조회
     */
    @Override
    public List<Todo> findByUUID(String uuid) {
        return queryFactory
                .selectFrom(todo)
                .where(todo.member.uuid.eq(UUID.fromString(uuid)))
                .fetch();
    }

    /**
     * 기한에 따른 투두 조회
     */
    @Override
    public List<Todo> findByDeadline(String uuid, Deadline deadLine, Integer page) {
        return queryFactory
                .selectFrom(todo)
                .where(todo.member.uuid.eq(UUID.fromString(uuid))
                        .and(todo.deadline.date.eq(deadLine.getDate())))
                .orderBy(todo.create_time.desc()) //생성한 날 기준 내림차순
                .offset(0+((page-1)*10)) //페이징 10개 불러오기
                .limit(10)
                .fetch();
    }

    /**
     * content가 포함된 투두 조회
     */
    @Override
    public List<Todo> findByContent(String uuid, String content, Integer page) {
        return queryFactory
                .selectFrom(todo)
                .where(todo.member.uuid.eq(UUID.fromString(uuid))
                        .and(todo.content.like("%"+content+"%")))
                .orderBy(todo.create_time.desc()) //생성한 날 기준 내림차순
                .offset(0+((page-1)*10)) //페이징 10개 불러오기
                .limit(10)
                .fetch();
    }

    /**
     * 권한 확인
     */
    @Override
    public boolean checkAuthorization(String uuid, Long id) {
        List<Todo> fetch = queryFactory
                .selectFrom(todo)
                .where(todo.member.uuid.eq(UUID.fromString(uuid))
                        .and(todo.id.eq(id)))
                .fetch();

        if(fetch.size()>0){
            return true;
        } else {
            return false;
        }
    }

    /**
     * 투두 삭제
     */
    @Override
    public Long delete(Long id) {
        queryFactory
                .delete(todo)
                .where(todo.id.eq(id))
                .execute();

        return id;
    }

}

 

[FindTodoDto.java]

package hooyn.todo.dto;

import hooyn.todo.domain.Deadline;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor
public class FindTodoDto {

    private Long id;

    private String title;
    private String content;

    private Deadline deadline;

    private LocalDateTime create_time;

    public FindTodoDto(Long id, String title, String content, Deadline deadline, LocalDateTime create_time) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.deadline = deadline;
        this.create_time = create_time;
    }
}

 

[TodoService.java]

package hooyn.todo.service;

import hooyn.todo.domain.Deadline;
import hooyn.todo.domain.Todo;
import hooyn.todo.dto.FindTodoDto;

import java.util.List;

public interface TodoService {

    //투두 작성
    Long writeTodo(Todo todo);

    //투두 조회 (todo_id)
    Todo findTodoById(Long todo_id);

    //투두 조회 (Deadline) //Dto를 통해서 데이터를 클라이언트에 반환 + 페이지 변수 추가
    List<FindTodoDto> findTodoByDeadline(String uuid, Deadline deadline, Integer page);

    //투두 조회 (Content) //Dto를 통해서 데이터를 클라이언트에 반환 + 페이지 변수 추가
    List<FindTodoDto> findTodoByContent(String uuid, String content, Integer page);

    //투두 수정
    Long updateTodo(Long todo_id, String title, String content, Deadline deadline);

    //투두 삭제
    Long deleteTodo(Long todo_id);

    //권한 확인
    boolean checkAuthorization(String uuid, Long todo_id);
}

 

[TodoServiceImpl.java]

package hooyn.todo.service;

import hooyn.todo.domain.Deadline;
import hooyn.todo.domain.Todo;
import hooyn.todo.dto.FindTodoDto;
import hooyn.todo.repository.TodoRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Primary
public class TodoServiceImpl implements TodoService {

    private final TodoRepository todoRepository;

    /**
     * 투두 작성
     */
    @Override
    @Transactional
    public Long writeTodo(Todo todo) {
        return todoRepository.save(todo);
    }

    /**
     * 투두 조회 (id)
     */
    @Override
    @Transactional(readOnly = true)
    public Todo findTodoById(Long todo_id) {
        return todoRepository.findById(todo_id);
    }

    /**
     * 투두 조회 (deadline)
     */
    @Override
    @Transactional(readOnly = true)
    public List<FindTodoDto> findTodoByDeadline(String uuid, Deadline deadline, Integer page) {
        List<Todo> todos = todoRepository.findByDeadline(uuid, deadline, page);
        return todos
                .stream()
                .map(todo -> new FindTodoDto( //DTO로 데이터를 받아서 저장하고 반환
                        todo.getId(),
                        todo.getTitle(),
                        todo.getContent(),
                        todo.getDeadline(),
                        todo.getCreate_time()))
                .collect(Collectors.toList());
    }

    /**
     * 투두 조회 (content)
     */
    @Override
    @Transactional(readOnly = true)
    public List<FindTodoDto> findTodoByContent(String uuid, String content, Integer page) {
        List<Todo> todos = todoRepository.findByContent(uuid, content, page);
        return todos
                .stream()
                .map(todo -> new FindTodoDto( //DTO로 데이터를 받아서 저장하고 반환
                        todo.getId(),
                        todo.getTitle(),
                        todo.getContent(),
                        todo.getDeadline(),
                        todo.getCreate_time()))
                .collect(Collectors.toList());
    }

    /**
     * 투두 삭제
     */
    @Override
    @Transactional
    public Long deleteTodo(Long todo_id) {
        return todoRepository.delete(todo_id);
    }

    /**
     * 투두 업데이트 (변경감지 사용)
     */
    @Override
    @Transactional
    public Long updateTodo(Long todo_id, String title, String content, Deadline deadline) {
        Todo todo = todoRepository.findById(todo_id);

        if(!title.isBlank()){
            todo.changeTitle(title);
        }

        if(!content.isBlank()){
            todo.changeContent(content);
        }

        if(deadline!=null || !deadline.getDate().isBlank()){
            todo.changeDeadline(deadline);
        }

        return todo_id;
    }

    /**
     * 투두 권한 확인
     */
    @Override
    public boolean checkAuthorization(String uuid, Long todo_id) {
        return todoRepository.checkAuthorization(uuid, todo_id);
    }
}

 

투두에 대한 도메인과 Repository, Service 코드를 구현하였습니다.
이번 프로젝트 부터 SRP와 DIP에 대한 개념을 이해하고
이를 적용하면서 프로젝트를 진행해보고 있습니다.

 

팀프로젝트 : Todo Code Github

 

GitHub - hooyn/Todo: [팀프로젝트] Todo App 서버 API

[팀프로젝트] Todo App 서버 API. Contribute to hooyn/Todo development by creating an account on GitHub.

github.com

728x90
반응형