: 애플리케이션에서 필요한 데이터를 제공해주기 위해 데이터를 보관/관리하는 역할.
: 지휘 역할, HTTP 요청을 받아 애플리케이션에 필요한 데이터를 Model에 담고 화면을 그릴 때 필요한 viewName(html 파일이름)을 View에 알려주는 역할.
: 화면에 그리는 역할, 화면에 데이터가 필요할시 Model에 담겨있는 데이터를 활용한다.

: 사용자의 요청을 처리하고 결과를 사용자에게 반환하는 역할. 비지니스 로직 처리가 필요한 경우 service 호출
: 비지니스 로직 처리 담당, 데이터 관련 처리가 필요할 경우 repository 호출
: 외부 데이터 소스와 상호작용 담당, 보통 CRUD 작업 수행

음 굉장히 많이 헤멨다. 중복 제거, 느슨한 결합으로 만들기 등을 하고 나서
Jdbc에서 JPA로 DB를 바꾸는 것을 해야했어서, 사실 정확하게 이해하지 못하고, 강의에서 예제로 준 것을 내 할 일 리스트 프로젝트에 대입해서 진행했다.
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;
}
build.gradle: dependencies에 추가하기
// JPA 설정
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
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();
}
}
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;
}
updatable = false 옵션을 추가한다.@SpringBootApplication 이 있는 class에 @EnableJpaAuditing 추가!
@EnableJpaAuditing 을 추가해야한다.
- SchedulerAppication
@EnableJpaAuditing @SpringBootApplication public class SchedulerApplication { public static void main(String[] args) { SpringApplication.run(SchedulerApplication.class, args); } }
ScheduleRepository가 JpaRepository<Schedule, Long> 상속을 받으면, 위에 적었던 길고 긴 Jdbc식 repository 코드가 없어도 된다. 당연함. JPA임.
JpaRepository 안에 쓸 수 있는 메서드들이 많고, 안에 메서드가 sql이 되도록 하는 메서드를 써주면 바로 sql 처럼 쓸 수 있다.
public interface ScheduleRepository extends JpaRepository<Schedule, Long> {...}
public ScheduleResponseDto createSchedule(ScheduleRequestDto requestDto) {
Schedule schedule = new Schedule(requestDto);
// DB 저장
Schedule saveSchedule = scheduleRepository.save(schedule);
ScheduleResponseDto scheduleResponseDto = new ScheduleResponseDto(schedule);
return scheduleResponseDto;
}

@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();
}

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("선택한 메도는 존재하지 않습니다. "));
}
: 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();
}
Optional<Schedule> findById(Long id);
해당 메서드는 Schedule 테이블에서 해당 id를 가진 Schedule을 가져오는 SQL을 실행하는 메서드를 생성할 수 있다.
Optional은 값이 있을 수도 있고 없을 수도 있는 상황을 표현하기 위해 사용된다. 즉, findById 메서드는 Schedule 객체를 반환할 수도 있고, 해당 id에 해당하는 엔티티가 없을 경우 빈 Optional을 반환할 수도 있다.
다시 한번 말해, 이 메서드는 데이터베이스에서 주어진 id에 해당하는 Schedule 객체를 찾고, 결과를 Optional로 반환한다. 결과가 존재하면 Optional에 Schedule 객체가 포함되고, 존재하지 않으면 빈 Optional이 반환된다.
public ScheduleResponseDto getOneSchedule(Long id) {
// DB 조회 (비밀번호 필드를 제외)
Schedule schedule = scheduleRepository.findById(id)
.orElseThrow(() -> new NoSuchElementException("해당하는 할 일이 존재하지 않습니다."));
return new ScheduleResponseDto(schedule);
}
List<Schedule> findByModifiedDateBetween(LocalDateTime start, LocalDateTime end);
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에 저장한다.
List<Schedule> findByManagerAndModifiedDateBetween(String manager, LocalDateTime start, LocalDateTime end);
if (modifiedDate != null && manager != null) {
schedules = scheduleRepository.findByManagerAndModifiedDateBetween(manager, modifiedDate.atStartOfDay(), modifiedDate.atStartOfDay().plusDays(1).minusNanos(1));
}
List<Schedule> findByManager(String manager);
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("선택한 메도는 존재하지 않습니다. "));
}
}