유미의 기록들

[개인 과제 - Spring입문] 일정 관리 앱 서버 본문

대외활동 기록/내일배움캠프

[개인 과제 - Spring입문] 일정 관리 앱 서버

지유미 2024. 8. 16. 10:06
728x90
반응형
💡구현하고자 하는 서비스의 전체적인 흐름을 파악하고 필요한 기능을 설계할 수 있다
💡API명세서, ERD, SQL 작성 할 수 있다
💡Spring Boot를 기반으로 CRUD 기능이 포함된 REST API 만들 수 있다

 

📌 요구사항 분석

일정 도메인 모델

  • 일정 ID
  • 담당자명
  • 비밀번호
  • 할 일
  • 작성/ 수정일

일정 관리 기능

  • 일정 등록
  • 선택한 일정 조회
  • 일정 목록 조회
  • 일정 수정 
  • 일정 삭제

*일정 수정, 삭제는 선택한 일정의 비밀번호가 일치할 경우에만 가능

*CRUD 필수 기능은 데이터베이스 연결 및 JDBC 사용해야 함

*일정 작성, 수정, 조회 시 반환 받은 일정 정보에 비밀번호는 제외함

 

💻 개발 과정

스프링 부트 스타터 사이트에서 스프링 프로젝트 생성

프로젝트 선택

  • Project : Gradle - Groovy
  • Language : Java
  • Spring Boot : 3.3.2

Project Metadata

  • Group: com.sparta
  • Artifact: schedulemanagement
  • Package name: com.sparta.schedulemanagement
  • Packaging : Jar
  • Java : 17

Dependencies

  • Spring Web
  • Thymeleaf
  • Lombok

 

0️⃣단계 | 요구사항 설계

본격적으로 개발하기 전, API 명세서와 ERD 설계를 통해 서비스가 제공해야 하는 기능을 명확히 하여 시스템의 복잡성을 관리하고, 개발 과정에서 발행할 수 있는 문제를 사전에 방지하고자 한다

 

API 명세서 작성

 

ERD 작성

 

 

SQL 작성

1. Schedule 데이터베이스 생성

create database schedule;
use schedule //데이터베이스 선택

2. Todo 테이블 생성

CREATE TABLE SCHEDULE(
    id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(15) NOT NULL,
    password VARCHAR(30) NOT NULL,
    todo VARCHAR(50) NULL,
    date DATETIME NULL,
    CONSTRAINT SCHEDULE_PK PRIMARY KEY(id)
);

 

1️⃣단계 | 일정 Entity, DTO 생성

API명세서와 ERD를 참고하여 Entity를 생성한다

 

Entity

@Data
public class Schedule {
    private Long id;
    private String name;
    private String password;
    private String todo;
    private String date;

    public Schedule(ScheduleRequestDto scheduleRequestDto){
        this.name= scheduleRequestDto.getName();
        this.password=scheduleRequestDto.getPassword();
        this.todo=scheduleRequestDto.getTodo();
        this.date=scheduleRequestDto.getDate();
    }
}

 

DTO 

@Getter
public class ScheduleRequestDto {
    private String name;
    private String password;
    private String todo;
    private String date;
}
@Getter
public class ScheduleResponseDto {
    private Long id;
    private String name;
    private String todo;
    private String date;

    public ScheduleResponseDto(Schedule schedule){
        this.id=schedule.getId();
        this.name=schedule.getName();
        this.todo=schedule.getTodo();
        this.date=schedule.getDate();
    }
    public ScheduleResponseDto(Long id,String name,String todo,String date){
        this.id=id;
        this.name=name;
        this.todo=todo;
        this.date=date;
    }
}

 

ScheduleController 생성 

Controller는 사용자의 요청이 진입하는 지점이다. 

@RestController
@RequestMapping("/api")
public class ScheduleController {
    private final ScheduleRepository scheduleRepository;
    @Autowired
    public ScheduleController(ScheduleRepository scheduleRepository){
        this.scheduleRepository=scheduleRepository;
    }
    //일정 등록
    @PostMapping("/schedules")
    public ScheduleResponseDto createSchedule(@RequestBody ScheduleRequestDto requestDto){
        //RequestDto -> Entity
        Schedule schedule=new Schedule(requestDto);
        //임시저장소 저장
        scheduleRepository.save(schedule);
        //Entity -> ResponseDto
        ScheduleResponseDto scheduleResponseDto=new ScheduleResponseDto(schedule);
        return scheduleResponseDto;
    }
}

 

ScheduleRepository 생성 (임시저장소 Map 사용)

@Repository
public class ScheduleRepository {
    //임시 저장소
    private static final Map<Long,Schedule> scheduleList=new HashMap<>();
    private static long sequence=0L;
    public void save(Schedule schedule){
        schedule.setId(++sequence);
        scheduleList.put(schedule.getId(),schedule);
    }
    public Schedule findById(Long id){
        return scheduleList.get(id);
    }
    public List<Schedule> findAll(){
        return new ArrayList<>(scheduleList.values());
    }
}

 

Postman으로 테스트 성공

 

 

2️⃣단계 | 데이터베이스 연동

실제 MySQL 데이터베이스 연동을 위해 JDBC를 사용하려고 한다

JDBC (Java Database Connectivity)
Java 애플리케이션에서 데이터베이스에 접근하고 데이터를 조작하기 위한 표준 API

 

application.properties 파일에서 데이터베이스 연결 정보를 설정한다

spring.application.name=schedule-management
#데이터베이스 URL
spring.datasource.url=jdbc:mysql://localhost:3306/schedule
#사용자이름
spring.datasource.username=root
#비밀번호
spring.datasource.password={비밀번호}
#드라이버 클래스 이름
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

 

build.gradle에 추가

implementation 'mysql:mysql-connector-java:8.0.28'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'

 

 

JDBC 템플릿 사용

일정 등록, 수정, 삭제 기능 구현 완료

public class ScheduleController {
    private final JdbcTemplate jdbcTemplate;
    
    public ScheduleController(JdbcTemplate jdbcTemplate){
        this.jdbcTemplate=jdbcTemplate;
    }
    
    @PostMapping("/schedules")
    public ScheduleResponseDto createSchedule(@RequestBody ScheduleRequestDto requestDto){
        //RequestDto -> Entity
        Schedule schedule=new Schedule(requestDto);
        
        //DB 저장
        KeyHolder keyHolder=new GeneratedKeyHolder(); //기본 키를 반환받기 위한 객체
        String sql="INSERT INTO schedule (name, password, todo, date) VALUES (?,?,?,?)";
        jdbcTemplate.update(con ->{
            PreparedStatement preparedStatement=con.prepareStatement(sql,
                    Statement.RETURN_GENERATED_KEYS);
            preparedStatement.setString(1,schedule.getName());
            preparedStatement.setString(2,schedule.getPassword());
            preparedStatement.setString(3,schedule.getTodo());
            preparedStatement.setString(4,schedule.getDate());
            return preparedStatement;
        },keyHolder);

        Long id=keyHolder.getKey().longValue();
        schedule.setId(id);

        //Entity -> ResponseDto
        ScheduleResponseDto scheduleResponseDto=new ScheduleResponseDto(schedule);
        return scheduleResponseDto;
    }
}

클라이언트로부터 Request메시지의 Body 정보를 받아서 로직을 처리하는 코드이다

JDBC템플릿을 사용하여 데이터베이스에 저장을 한 후, Response 메시지로 ResponseDto를 응답으로 반환한다

 

여기서는 Contoller 클래스에 클라이언트로의 요청을 받고, 로직을 처리하며, 데이터베이스로 저장하는 코드를 전부 포함되어 있기 때문에 너무 많은 역할을 담당하고 있어 유지보수성, 확장성, 테스트 용이성 측면에서 좋지 않다 

 

3️⃣단계 | 3 Layer Architecture

3 Layer Architecture에 따라 각 레이어를 분리하려고 한다

Contoller

@RestController
@RequestMapping("/api")
public class ScheduleController {
    private final JdbcTemplate jdbcTemplate;

    public ScheduleController(JdbcTemplate jdbcTemplate){
        this.jdbcTemplate=jdbcTemplate;
    }
    //일정 등록
    @PostMapping("/schedules")
    public ScheduleResponseDto createSchedule(@RequestBody ScheduleRequestDto requestDto){
        ScheduleService scheduleService=new ScheduleService(jdbcTemplate);
        return scheduleService.createSchedule(requestDto);
    }

    //선택한 일정 조회
    @GetMapping("/schedules/{id}")
    public ScheduleResponseDto getSchedule(@PathVariable Long id){
        ScheduleService scheduleService=new ScheduleService(jdbcTemplate);
        return scheduleService.getSchedule(id);
    }

    //전체 일정 목록 조회
    @GetMapping("/schedules")
    public List<ScheduleResponseDto> getScheduleList(){
        ScheduleService scheduleService=new ScheduleService(jdbcTemplate);
        return scheduleService.getScheduleList();
    }

    //선택한 일정 수정
    @PutMapping("/schedules/{id}")
    public Long updateSchedule(@PathVariable Long id,@RequestBody ScheduleRequestDto scheduleRequestDto){
        ScheduleService scheduleService=new ScheduleService(jdbcTemplate);
        return scheduleService.updateSchedule(id,scheduleRequestDto);
    }

    //선택한 일정 삭제
    @DeleteMapping("/schedules/{id}")
    public Long deleteSchedule(@PathVariable Long id){
        ScheduleService scheduleService=new ScheduleService(jdbcTemplate);
        return scheduleService.deleteSchedule(id);
    }
}

 

Service

public class ScheduleService {
    private final JdbcTemplate jdbcTemplate;

    public ScheduleService(JdbcTemplate jdbcTemplate){
        this.jdbcTemplate=jdbcTemplate;
    }
    //일정 등록
    public ScheduleResponseDto createSchedule(ScheduleRequestDto requestDto) {
        //RequestDto -> Entity
        Schedule schedule=new Schedule(requestDto);

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

        //Entity -> ResponseDto
        ScheduleResponseDto scheduleResponseDto=new ScheduleResponseDto(schedule);
        return scheduleResponseDto;
    }
    //선택한 일정 조회
    public ScheduleResponseDto getSchedule(Long id) {
        //해당 일정이 존재하는 지 확인
        ScheduleRepository scheduleRepository=new ScheduleRepository(jdbcTemplate);
        Schedule schedule=scheduleRepository.findById(id);
        if(schedule!=null) {
            ScheduleResponseDto scheduleResponseDto=scheduleRepository.find(id);
            return scheduleResponseDto;
        }else{
            throw new IllegalArgumentException("선택한 일정은 존재하지 않습니다");
        }
    }
    //일정 수정
    public Long updateSchedule(Long id, ScheduleRequestDto scheduleRequestDto) {
        //해당 일정이 DB에 존재하는 지 확인
        ScheduleRepository scheduleRepository=new ScheduleRepository(jdbcTemplate);
        Schedule schedule=scheduleRepository.findById(id);
        if(schedule!=null){
           scheduleRepository.updateSchedule(id,scheduleRequestDto);
            return id;
        }else{
            throw new IllegalArgumentException("선택한 일정은 존재하지 않습니다");
        }
    }
    //전체 일정 조회
    public List<ScheduleResponseDto> getScheduleList() {
        ScheduleRepository scheduleRepository=new ScheduleRepository(jdbcTemplate);
        return scheduleRepository.findAll();
    }
    //일정 삭제
    public Long deleteSchedule(Long id) {
        ScheduleRepository scheduleRepository=new ScheduleRepository(jdbcTemplate);

        Schedule schedule=scheduleRepository.findById(id);
        //해당 일정이 DB에 존재하는 지 확인
        if(schedule!=null){
            scheduleRepository.delete(id);
            return id;
        }else{
            throw new IllegalArgumentException("선택한 일정은 존재하지 않습니다");
        }
    }
    public Schedule findById(Long id){
        String sql="SELECT * FROM schedule WHERE id = ?";

        return jdbcTemplate.query(sql,resultSet ->{
            if(resultSet.next()){
                Schedule schedule=new Schedule();
                schedule.setName(resultSet.getString("name"));
                schedule.setTodo(resultSet.getString("todo"));
                return schedule;
            }else{
                return null;
            }
        },id);
    }
}

 

Repository

public class ScheduleRepository {
    private final JdbcTemplate jdbcTemplate;
    public ScheduleRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate=jdbcTemplate;
    }
    //DB저장
    public Schedule save(Schedule schedule) {
        KeyHolder keyHolder=new GeneratedKeyHolder(); //기본 키를 반환받기 위한 객체

        String sql="INSERT INTO schedule (name, password, todo, date) VALUES (?,?,?,?)";

        jdbcTemplate.update(con ->{
            PreparedStatement preparedStatement=con.prepareStatement(sql,
                    Statement.RETURN_GENERATED_KEYS);

            preparedStatement.setString(1,schedule.getName());
            preparedStatement.setString(2,schedule.getPassword());
            preparedStatement.setString(3,schedule.getTodo());
            preparedStatement.setString(4,schedule.getDate());

            return preparedStatement;
        },keyHolder);

        Long id=keyHolder.getKey().longValue();
        schedule.setId(id);
        return schedule;
    }
    //선택한 id 조회
    public Schedule findById(Long id){
        String sql="SELECT * FROM schedule WHERE id = ?";

        return jdbcTemplate.query(sql,resultSet ->{
            if(resultSet.next()){
                Schedule schedule=new Schedule();
                schedule.setName(resultSet.getString("name"));
                schedule.setTodo(resultSet.getString("todo"));
                return schedule;
            }else{
                return null;
            }
        },id);
    }
    //전체 조회
    public List<ScheduleResponseDto> findAll() {
        //DB 조회
        String sql="SELECT * FROM schedule";

        return jdbcTemplate.query(sql, new RowMapper<ScheduleResponseDto>() {
            @Override
            public ScheduleResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
                Long id=rs.getLong("id");
                String name=rs.getString("name");
                String todo=rs.getString("todo");
                String date=rs.getString("date");
                return new ScheduleResponseDto(id,name,todo,date);
            }
        });
    }
    //삭제
    public void delete(Long id) {
        String sql="DELETE FROM schedule WHERE id = ?";
        jdbcTemplate.update(sql,id);
    }
    //수정
    public void updateSchedule(Long id, ScheduleRequestDto scheduleRequestDto) {
        String sql="UPDATE schedule SET name = ?, todo = ? WHERE id = ?";
        jdbcTemplate.update(sql,scheduleRequestDto.getName(),scheduleRequestDto.getTodo(),id);
    }
    public ScheduleResponseDto find(Long id) {
        String sql="SELECT * FROM schedule WHERE id = ?";
        return jdbcTemplate.queryForObject(sql, new RowMapper<ScheduleResponseDto>() {
            @Override
            public ScheduleResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
                return new ScheduleResponseDto(
                        rs.getLong("id"),
                        rs.getString("name"),
                        rs.getString("todo"),
                        rs.getString("date")
                );
            }
        },id);
    }
}

Controller는 사용자 요청을 받아서 어떤 서비스 메서드를 호출할 지 결정하는 역할만 하고, Service는 비즈니스 로직을 처리하며 Repository는 데이터베이스와의 상호작용만을 담당한다

 

 

4️⃣단계 | IoC, DI

ScheduleController에서 ScheduleService 객체 생성 부분이나 ScheduleService에서 ScheduleRepository객체 생성 부분이 메소드마다 중복되는 것을 볼 수 있다

ScheduleRepository scheduleRepository=new ScheduleRepository(jdbcTemplate);

 

Controller -> Service -> Repository 강한 결합이 생기는데 이를 약한 결합으로 만들어 주기 위해 외부에서 객체를 생성해서 생성자의 파라미터로 받아오는 생성자 주입으로 구현하였다

@Component로 IoC Container에 Bean 등록하고 @Autoweird로 생성자 주입을 한다

(@Repository로 명시적으로 표기할 수 있다 @Component가 포함되어 있기 때문)

private final ScheduleRepository scheduleRepository;

@Autowired
public ScheduleService(ScheduleRepository scheduleRepository){
    this.scheduleRepository=scheduleRepository;
}
@Repository
public class ScheduleRepository {
	...
}

 

 

📝알게 된 부분

DTO (Data Transfer Object, 데이터 전송 객체)
계층 간 데이터를 전달하기 위해 사용하는 객체, Controller는 View와 도메인 Model의 데이터를 주고 받을 때 별도의 DTO를 주로 사용한다. 도메인 객체를 View에 직접 전달할 수 있지만, 민감한 도메인 비즈니스 기능이 노출될 수 있으며 Model과 View 사이에 의존성이 생기기 때문이다

 

 

 

 

728x90
반응형
Comments