[내배캠 Java 6기] MVC와 과제 피드백 수용+JPA로 바꾸기

ekkkyo_j·2024년 8월 20일

[내배캠 Java 6기] TIL

목록 보기
22/38

📌 MVC

1. Model

: 애플리케이션에서 필요한 데이터를 제공해주기 위해 데이터를 보관/관리하는 역할.

2. View

: 지휘 역할, HTTP 요청을 받아 애플리케이션에 필요한 데이터를 Model에 담고 화면을 그릴 때 필요한 viewName(html 파일이름)을 View에 알려주는 역할.

3. Controller

: 화면에 그리는 역할, 화면에 데이터가 필요할시 Model에 담겨있는 데이터를 활용한다.

📌 3 - 계층 구조

1. Controller

: 사용자의 요청을 처리하고 결과를 사용자에게 반환하는 역할. 비지니스 로직 처리가 필요한 경우 service 호출

2. Service

: 비지니스 로직 처리 담당, 데이터 관련 처리가 필요할 경우 repository 호출

3. Repository

: 외부 데이터 소스와 상호작용 담당, 보통 CRUD 작업 수행

📌 할 일 리스트 만드는 과제 디벨롭하기

음 굉장히 많이 헤멨다. 중복 제거, 느슨한 결합으로 만들기 등을 하고 나서
Jdbc에서 JPA로 DB를 바꾸는 것을 해야했어서, 사실 정확하게 이해하지 못하고, 강의에서 예제로 준 것을 내 할 일 리스트 프로젝트에 대입해서 진행했다.

1. 느슨한 결합 만들기(Bean과 IoC container)

ScheduleController에서 ScheduleService, ScheduleRepository 꺼내와 3 - 계층 구조를 만들어주었다. 이때, Jdbc template을 Controller, Service, Repository에 선언하여 Controller에서 Service를 선언할 때 Jdbc template을 생성자 값으로 넣도록, Service에서 Repository를 선언할 때에도 같은 방식으로 하여

public class ScheduleService {
    private final ScheduleRepository scheduleRepository;
    public ScheduleService(JdbcTemplate jdbcTemplate) {
        this.scheduleRepository = new ScheduleRepository(jdbcTemplate);
    }
    ...
 }

강한 결합으로 3 - 계층 구조가 되어있었다. 이를, 느슨한 결합으로 바꿔주었다.
강한 결합은 Controller 내부에서 Service를 생성하고, Service 내부에서 Repository를 생성해주어 발생했다. 그럼 느슨한 결합은 Controller 외부에서 Service를 생성하고, Service 외부에서 Repository를 생성하여 받아와주기만 하면 되는 것이다. 그런데, 이들을 생성하는 것을 누가 해주느냐?
이는 Bean과 IoC container로 해결된다. Controller 위에 @Controller를 Service 위에 @Service를 Repository 위에 @Repository를 해주어, Spring 자체에서 관리하는 Component로 바꾸어주면 된다. 이들은 IoC container에 저장되어 우리가 따로 생성해주지 않아도, 알아서 생성해준다.
따라서 우리는 "외부에" Controller, Service, Repository를 생성하였음으로 느슨한 결합을 만족시켰다.

ScheduleController : @Service(@Component인) Service를 외부에서 받아온다.

@RestController
@RequestMapping("/api")
public class ScheduleController {
    //JDBC DB 생성

    private final ScheduleService scheduleService;
    public ScheduleController(ScheduleService scheduleService) {
        this.scheduleService = scheduleService;
    }

ScheduleService : @Repository(@Component인) Repository를 외부에서 받아온다.

@Service
public class ScheduleService {
    private final ScheduleRepository scheduleRepository;
    public ScheduleService(ScheduleRepository scheduleRepository) {
        this.scheduleRepository = scheduleRepository;
    }

ScheduleRepository: JdbcTemplate을 가져온다.

@Repository
public class ScheduleRepository {
    private final JdbcTemplate jdbcTemplate;
    public ScheduleRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

2. 환경설정

build.gradle: dependencies에 추가하기

	// JPA 설정
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

3. Schedule Entity 수정

Schedule : JPA식 column 등록

package com.sparta.scheduler.entity;

import com.sparta.scheduler.dto.ScheduleRequestDto;
import jakarta.persistence.*;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.Date;
import java.time.LocalDateTime;

@Entity
@Getter
@Setter
@Table(name="schedule")
@NoArgsConstructor
public class Schedule {
    //할일, 담당자명, 비밀번호, 작성/수정일
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name="contents", nullable = false, length = 500)
    private String contents;

    @Column(name="manager", nullable = false)
    private String manager;

    @Column(name="password", nullable = false, length = 200)
    private String password;

    @Column(name="createTime", nullable = false)
    private LocalDateTime createTime;

    @Column(name="modifiedTime", nullable = false)
    private LocalDateTime modifiedTime;

    public Schedule(ScheduleRequestDto requestDto) {
        this.contents = requestDto.getContents();
        this.manager = requestDto.getManager();
        this.password = requestDto.getPassword();
        this.createTime = LocalDateTime.now();
        this.modifiedTime = createTime;
    }

    // update 수정파트
    public void update(ScheduleRequestDto requestDto) {
        this.contents = requestDto.getContents();
    }

}

4. JPA Auditing - Timestamped 클래스

JPA Auditing을 사용하여 createDate, modifiedDate를 따로 클래스로 빼주었다.
이를 Schedule entity가 상속하도록 해준다.

package com.sparta.scheduler.entity;

import jakarta.persistence.*;
import jakarta.persistence.TemporalType;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class) // 자동으로 시간을 넣어줌
public abstract class Timestamped {

    @CreatedDate
    @Column(updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime createDate;

    @LastModifiedDate
    @Column
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime modifiedDate;
}
  • Spring Data JPA에서는 시간에 대해서 자동으로 값을 넣어주는 기능인 JPA Auditing을 제공하고 있다.
  • @MappedSuperclass
    • JPA Entity 클래스들이 해당 추상 클래스를 상속할 경우 createDate, modifiedDate 처럼 추상 클래스에 선언한 멤버변수를 컬럼으로 인식할 수 있다.
    • 여기에서는 Timestamped 클래스가 추상 클래스이고 이를 Schedule entity 클래스가 상속받는다.
  • @EntityListeners(AuditingEntityListener.class)
    • 해당 클래스에 Auditing 기능을 포함시켜 준다.
  • @CreatedDate
    • Entity 객체가 생성되어 저장될 때 시간이 자동으로 저장된다.
    • 최초 생성 시간이 저장되고 그 이후에는 수정되면 안되기 때문에 updatable = false 옵션을 추가한다.
  • @LastModifiedDate
    • 조회한 Entity 객체의 값을 변경할 때 변경된 시간이 자동으로 저장된다.
    • 처음 생성 시간이 저장된 이후 변경이 일어날 때마다 해당 변경시간으로 업데이트된다.
  • @Temporal
    • 날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용한다.
    • DB에는 Date(날짜), Time(시간), Timestamp(날짜와 시간)라는 세 가지 타입이 별도로 존재합니다.
      • DATE : ex) 2023-01-01
      • TIME : ex) 20:21:14
      • TIMESTAMP : ex) 2023-01-01 20:22:38.771000

🔥 JPA Auditing 설정

@SpringBootApplication 이 있는 class에 @EnableJpaAuditing 추가!

  • JPA Auditing 기능을 사용하겠다는 정보를 전달해주기 위해 @EnableJpaAuditing 을 추가해야한다.
  • SchedulerAppication
@EnableJpaAuditing
@SpringBootApplication
public class SchedulerApplication {
	public static void main(String[] args) {
		SpringApplication.run(SchedulerApplication.class, args);
	}
}

5. 이후로 굉장한 시행착오

ScheduleRepository가 JpaRepository<Schedule, Long> 상속을 받으면, 위에 적었던 길고 긴 Jdbc식 repository 코드가 없어도 된다. 당연함. JPA임.

JpaRepository 안에 쓸 수 있는 메서드들이 많고, 안에 메서드가 sql이 되도록 하는 메서드를 써주면 바로 sql 처럼 쓸 수 있다.

✨ jpaRepository 등록

public interface ScheduleRepository extends JpaRepository<Schedule, Long> {...}
  • JpaRepository<"@Entity 클래스", "@Id 의 데이터 타입">를 상속받는 interface 로 선언.
    • Spring Data JPA에 의해 자동으로 Bean 등록이 되었다.
    • 제네릭스의 @Entity 클래스 위치에 Memo Entity를 추가했기 때문에 해당 MemoRepository는 DB의 memo 테이블과 연결되어 CRUD 작업을 처리하는 인터페이스가 되었다.

✨ save

  • ScheduleService
    public ScheduleResponseDto createSchedule(ScheduleRequestDto requestDto) {

        Schedule schedule = new Schedule(requestDto);

        // DB 저장
        Schedule saveSchedule = scheduleRepository.save(schedule);

        ScheduleResponseDto scheduleResponseDto = new ScheduleResponseDto(schedule);

        return scheduleResponseDto;
    }

  • SimpleJpaRepository의 save 메서드를 확인해보면 영속성 컨텍스트에 entity를 저장하는 코드가 작성되어있다.
  • save 메서드를 사용해 데이터를 저장할 수 있다.
    • 파라미터로는 저장하려는 entity 객체를 넣어주면 된다.
    • 해당 메서드에 @Transactional이 적용되어있는 것을 확인할 수 있다.

✨ update

  • ScheduleService
    : SimpleJpaRepository에 update라는 메서드는 존재하지 않는다. id, password, requestDto를 입력 받아 해당 id를 가진 schedule이 있는지 생성한 findSchedule 메서드를 호출해서 해당 Schedule를 찾고 받은 비밀번호가 schedule의 비밀번호와 같은 지 확인하여 같은 경, schedule.update(requestDto)하여 받은 requestDto로 schedule를 수정한다. 그리고 public ScheduleResponseDto updateSchedule보면 반환값이 ScheduleResponseDto이기 때문에 return new ScheduleResponseDto(schedule) 바꿔서 반환해준다.
    @Transactional
    public ScheduleResponseDto updateSchedule(Long id, String password, ScheduleRequestDto requestDto) {

        // 해당 메모가 DB에 존재하는지 확인
        Schedule schedule = findSchedule(id);
        if(password.equals(schedule.getPassword())) {
            // memo 내용 수정
            schedule.update(requestDto);

            return new ScheduleResponseDto(schedule);
        } else {
            throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
        }
    }
	private Schedule findSchedule(Long id) {
        return scheduleRepository.findById(id).orElseThrow(() ->
                new IllegalArgumentException("선택한 메도는 존재하지 않습니다. "));
    }

SimpleJpaRepository에 update라는 메서드는 존재하지 않는다. 따라서, 위 코드처럼 영속성 컨텍스트의 변경감지를 통해 update를 진행한다.변경감지가 적용되기 위해 해당 메서드에 @Transactional을 추가하였다. update 메서드는 schdule entity에 구현되어있다.

    // update 수정파트
    public void update(ScheduleRequestDto requestDto) {
        this.contents = requestDto.getContents();
        this.manager = requestDto.getManager();
    }

✨ Delete

  • ScheduleService
    : SimpleJpaRepository에 delete 메서드가 존재한다.
    id와 password를 입력받아 findSchedule(id)로 id에 해당하는 schedule를 찾고 입력받은 비밀번호와 해당 schedule의 비밀번호가 같은 지 확인하여 같은 경우, scheduleRepository.delete(schedule);로 delete 메서드를 사용해 해당 Entity(데이터)를 테이블에서 삭제한다. 파라미터로는 삭제하려는 entity 객체를 넣어주면 된다. delete 메서드에 @Transactional이 적용되어있는 것을 확인할 수 있다.
   public Long deleteSchedule(Long id, String password) {

        // 해당 메모가 DB에 존재하는지 확인
        Schedule schedule = findSchedule(id);
        if(password.equals(schedule.getPassword())) {
            // schedule 삭제
            scheduleRepository.delete(schedule);

            return id;
        } else {
            throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
        }
    }
   private Schedule findSchedule(Long id) {
        return scheduleRepository.findById(id).orElseThrow(() ->
                new IllegalArgumentException("선택한 메도는 존재하지 않습니다. "));
    }

👉🏻 Query Methods란?

: Spring Data JPA에서는 메서드 이름으로 SQL을 생성할 수 있는 Query Methods 기능을 제공한다. JpaRepository 인터페이스에서 해당 인터페이스와 매핑되어있는 테이블에 요청하고자하는 SQL을 메서드 이름을 사용하여 선언할 수 있다.

package com.sparta.memo.repository;

import com.sparta.memo.entity.Memo;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface MemoRepository extends JpaRepository<Memo, Long> {
    List<Memo> findAllByOrderByModifiedAtDesc();
}
  • SimpleJpaRepository 클래스가 생성될 때 위처럼 직접 선언한 JpaRepository 인터페이스의 모든 메서드를 자동으로 구현해준다.
    • JpaRepository 인터페이스의 메서드 즉, Query Methods는 개발자가 이미 정의 되어있는 규칙에 맞게 메서드를 선언하면 해당 메서드 이름을 분석하여 SimpleJpaRepository에서 구현이 된다.
    • 따라서 우리는 인터페이스에 필요한 SQL에 해당하는 메서드 이름 패턴으로 메서드를 선언 하기만 하면 따로 구현하지 않아도 사용할 수 있다.

✨ findById

  • ScheduleRepository
Optional<Schedule> findById(Long id);

해당 메서드는 Schedule 테이블에서 해당 id를 가진 Schedule을 가져오는 SQL을 실행하는 메서드를 생성할 수 있다.

👉🏻 Optional

Optional은 값이 있을 수도 있고 없을 수도 있는 상황을 표현하기 위해 사용된다. 즉, findById 메서드는 Schedule 객체를 반환할 수도 있고, 해당 id에 해당하는 엔티티가 없을 경우 빈 Optional을 반환할 수도 있다.
다시 한번 말해, 이 메서드는 데이터베이스에서 주어진 id에 해당하는 Schedule 객체를 찾고, 결과를 Optional로 반환한다. 결과가 존재하면 Optional에 Schedule 객체가 포함되고, 존재하지 않으면 빈 Optional이 반환된다.

  • ScheduleService
    위에서 선언한 findById를 사용하여 id에 해당하는 schedule를 받아 schedule에 저장한다. orElseThrow()는 에러가 발생한 경우 () 안에 해당 Exception을 처리해주면 된다. NoSuchElementException은 Optional과 같은 요소를 다룰 때 해당 요소가 존재하지 않을 때 발생하는 Exception이다. 반환 값이 ScheduleResponseDto이므로 schedule을 ScheduleResponseDto(schedule)로 하여 ScheduleResponseDto로 바꿔주고 반환한다.
    public ScheduleResponseDto getOneSchedule(Long id) {
        // DB 조회 (비밀번호 필드를 제외)
        Schedule schedule = scheduleRepository.findById(id)
                .orElseThrow(() -> new NoSuchElementException("해당하는 할 일이 존재하지 않습니다."));

        return new ScheduleResponseDto(schedule);
    }

✨ findByModifiedDateBetween

  • ScheduleRepository
    : 해당 메서드는 yyyy-mm-dd를 입력 받았을 때 해당 날짜를 가진 schedule들을 가져오는 메서드이다. 그런데 왜 start, end를 받느냐하면, DB에 modifiedDate, createDate 모두 LocalDateTime 형이다. 즉, 시간까지 저장되어있다. 하지만, 날짜를 가지고서만 조회를 하고 싶기 때문에 yyyy-mm-dd의 00시 00분 00초부터 23시 59분 59.999999999초 사이의 modifiedDate값을 가진 Schedule를 리스트로 반환하는 메서드를 작성해야했다.
List<Schedule> findByModifiedDateBetween(LocalDateTime start, LocalDateTime end);
  • ScheduleService
else if (modifiedDate != null) {
  	schedules = scheduleRepository.findByModifiedDateBetween(modifiedDate.atStartOfDay(), modifiedDate.atStartOfDay().plusDays(1).minusNanos(1));
}

⚒️ modifiedDate.atStartOfDay()
: 수정일의 시작하는 순간, yyyy-mm-dd의 00시 00분 00초

⚒️ modifiedDate.atStartOfDay().plusDays(1).minusNanos(1)
: 수정일의 마지막 순간, yyyy-mm-dd의 23시 59분 59.999999999초

findByModifiedDateBetween 메서드는 List를 반환하기 때문에, 이 시간 사이의 값을 가진 schedule을 List형인 schedules에 저장한다.

✨ findByManagerAndModifiedDateBetween

  • ScheduleRepository
    : 특정 Manger와 특정 ModifiedDate를 가지 할 일들을 List로 가져오는 SQL를 실행하는 메서드를 생성한다.
List<Schedule> findByManagerAndModifiedDateBetween(String manager, LocalDateTime start, LocalDateTime end);
  • ScheduleService
    : modifiedDate, manager 모두 값이 들어왔을 때, 조건에 만족하는 해당 schedule들이 있는 리스트를 List schedules에 반환한다.
if (modifiedDate != null && manager != null) {
    schedules = scheduleRepository.findByManagerAndModifiedDateBetween(manager, modifiedDate.atStartOfDay(), modifiedDate.atStartOfDay().plusDays(1).minusNanos(1));
}

✨ findByManager

  • ScheduleRepository
    : 특정 Manger를 가진 schedule들을 List로 가져오는 SQL를 실행하는 메서드를 생성한다.
List<Schedule> findByManager(String manager);
  • ScheduleService
else if (manager != null) {
     schedules = scheduleRepository.findByManager(manager);
}

위에 꺼를 다 합쳐서, 처리한다. List 이므로 List을 바꿔줘야한다. schedules.stream().map(ScheduleResponseDto::new).toList();을 사용하여 바꿔준다.

public List<ScheduleResponseDto> getSchedules(LocalDate modifiedDate, String manager) {
        List<Schedule> schedules;
        if (modifiedDate != null && manager != null) {
            schedules = scheduleRepository.findByManagerAndModifiedDateBetween(manager, modifiedDate.atStartOfDay(), modifiedDate.atStartOfDay().plusDays(1).minusNanos(1));
        }
        else if (modifiedDate != null) {
            schedules = scheduleRepository.findByModifiedDateBetween(modifiedDate.atStartOfDay(), modifiedDate.atStartOfDay().plusDays(1).minusNanos(1));
        }
        else if (manager != null) {
            schedules = scheduleRepository.findByManager(manager);
        }
        else {
            schedules = scheduleRepository.findAll();
        }
        return schedules.stream().map(ScheduleResponseDto::new).toList();
    }

ScheduleRepository

package com.sparta.scheduler.repository;

import com.sparta.scheduler.entity.Schedule;
import org.springframework.data.jpa.repository.JpaRepository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

// ScheduleRepository.java
public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
    Optional<Schedule> findById(Long id);
    List<Schedule> findByModifiedDateBetween(LocalDateTime start, LocalDateTime end);
    List<Schedule> findByManager(String manager);
    List<Schedule> findByManagerAndModifiedDateBetween(String manager, LocalDateTime start, LocalDateTime end);
}

ScheduleService

package com.sparta.scheduler.service;

import com.sparta.scheduler.dto.ScheduleRequestDto;
import com.sparta.scheduler.dto.ScheduleResponseDto;
import com.sparta.scheduler.entity.Schedule;
import com.sparta.scheduler.repository.ScheduleRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.List;
import java.util.NoSuchElementException;

@Service
public class ScheduleService {
    private final ScheduleRepository scheduleRepository;
    public ScheduleService(ScheduleRepository scheduleRepository) {
        this.scheduleRepository = scheduleRepository;
    }


    public ScheduleResponseDto createSchedule(ScheduleRequestDto requestDto) {

        Schedule schedule = new Schedule(requestDto);

        // DB 저장
        Schedule saveSchedule = scheduleRepository.save(schedule);

        ScheduleResponseDto scheduleResponseDto = new ScheduleResponseDto(schedule);

        return scheduleResponseDto;
    }

    public ScheduleResponseDto getOneSchedule(Long id) {
        // DB 조회 (비밀번호 필드를 제외)
        Schedule schedule = scheduleRepository.findById(id)
                .orElseThrow(() -> new NoSuchElementException("해당하는 할 일이 존재하지 않습니다."));

        return new ScheduleResponseDto(schedule);
    }

    public List<ScheduleResponseDto> getSchedules(LocalDate modifiedDate, String manager) {
        List<Schedule> schedules;
        if (modifiedDate != null && manager != null) {
            schedules = scheduleRepository.findByManagerAndModifiedDateBetween(manager, modifiedDate.atStartOfDay(), modifiedDate.atStartOfDay().plusDays(1).minusNanos(1));
        }
        else if (modifiedDate != null) {
            schedules = scheduleRepository.findByModifiedDateBetween(modifiedDate.atStartOfDay(), modifiedDate.atStartOfDay().plusDays(1).minusNanos(1));
        }
        else if (manager != null) {
            schedules = scheduleRepository.findByManager(manager);
        }
        else {
            schedules = scheduleRepository.findAll();
        }
        return schedules.stream().map(ScheduleResponseDto::new).toList();
    }


    @Transactional
    public ScheduleResponseDto updateSchedule(Long id, String password, ScheduleRequestDto requestDto) {

        // 해당 메모가 DB에 존재하는지 확인
        Schedule schedule = findSchedule(id);
        if(password.equals(schedule.getPassword())) {
            // memo 내용 수정
            schedule.update(requestDto);

            return new ScheduleResponseDto(schedule);
        } else {
            throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
        }
    }


    public Long deleteSchedule(Long id, String password) {

        // 해당 메모가 DB에 존재하는지 확인
        Schedule schedule = findSchedule(id);
        if(password.equals(schedule.getPassword())) {
            // schedule 삭제
            scheduleRepository.delete(schedule);

            return id;
        } else {
            throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
        }
    }

    private Schedule findSchedule(Long id) {
        return scheduleRepository.findById(id).orElseThrow(() ->
                new IllegalArgumentException("선택한 메도는 존재하지 않습니다. "));
    }


}
profile
게으르지만 성실히 공부하기^_^

0개의 댓글