코딩/sparta TIL

CH3 일정 관리 앱 만들기

americanoallday 2025. 3. 24. 20:37

😳 코드내 인터페이스로 메서드 인식이 가능한 이유

아래 구조를 보면 이런식으로 선언하고 써야하지 않나 생각했는데

private final JdbcTemplateMemo memoRepository;

 

🌱 가능한 이유

바로 스프링의 DI(의존성 주입) + 빈 자동 등록 구조 때문.

👉 한마디로 말하면: “인터페이스 타입으로 선언해도, 그걸 구현한 클래스(@Repository 붙은 것)를 스프링이 자동으로 찾아서 주입해줌!”

 

여기서 의문이 생겼는데

🔍 의문: “근데 MemoRepository 인터페이스를 사용한 구현체가 여러 개면 어떡하나?”

찾아보니 해결방법이 있음.

 

✅ 해결 방법 3가지 (실무에서도 사용)

  • @Primary :하나를 "우선 주입 대상"으로 설정
  • @Qualifier("이름") : 명시적으로 어떤 빈을 넣을지 지정
  • 수동 빈 등록 : Java Config로 직접 빈 생성 및 주입
더보기

🎯 예시 - @Primary 사용

@Repository
@Primary
public class JdbcTemplateMemoRepository implements MemoRepository {
  ...
}

✔️ 이렇게 하면 다른 구현체가 있더라도 얘가 기본값이 됨

 

🎯 예시 - @Qualifier 사용

public class MemoServiceImpl {

  private final MemoRepository memoRepository;

  public MemoServiceImpl(@Qualifier("jdbcTemplateMemoRepository") MemoRepository memoRepository) {
    this.memoRepository = memoRepository;
  }
}

JDBC Template을 활용한 DB 연동 코드 구조 이해하기

🔍 코드 원문

private final JdbcTemplate jdbcTemplate;

public JdbcTemplateMemoRepository(DataSource dataSource) {
    this.jdbcTemplate = new JdbcTemplate(dataSource);
}

 

 

1. 스프링이 JdbcTemplateMemoRepository를 만들 때

2. DataSource를 자동으로 주입해줌 DataSource는 DB 연결 정보를 담고 있는 객체

3. 이걸 이용해서 JdbcTemplate 객체를 새로 만들어서 필드로 전달.

 

🔗 쉽게 말하면 

1. 스프링이 DataSource를 주입해줌

2. 이걸 가지고 JdbcTemplate을 만들어서

3. 그걸 이용해 SQL 실행 (jdbcTemplate.query(), update() 등)

 

🧠 비유로 하면?

“DataSource는 DB 연결선”이고 “JdbcTemplate은 그 연결선을 통해 SQL을 실행하는 도구”

 

💡 예시 코드 (이후에 이렇게 쓰임)

String sql = "SELECT * FROM memo WHERE id = ?";
Memo memo = jdbcTemplate.queryForObject(sql, memoRowMapper(), id);

 

💬 JdbcTemplate?
스프링에서 JDBC를 쉽게 사용하도록 도와주는 유틸 클래스
SQL을 직접 쿼리문으로 작성해서 실행할 수 있게 도와줌 (예: INSERT, SELECT 등)

코드 해석

Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters)); 

 

💡 Number는 자바의 추상 클래스

Integer, Long, Double, Float 모든 숫자 타입의 부모 타입

 

✅ executeAndReturnKey(...)

👉 DB에 insert 하고, AUTO_INCREMENT 또는 SERIAL 같은 자동 생성된 id 값을 반환해주는 메서드

→ insert 성공 후, 그 새로 생성된 id(PK)를 key 변수에 저장

 

✅ MapSqlParameterSource

📦 Map<String, Object>를 SQL 파라미터로 변환해주는 래퍼 클래스


RowMapper<MemoResponseDto> memoRowMapper() 메서드 해석

    private RowMapper<MemoResponseDto> memoRowMapper() {
        return new RowMapper<MemoResponseDto>() {
            @Override
            public MemoResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
                return new MemoResponseDto(
                        rs.getLong("id"),
                        rs.getString("title"),
                        rs.getString("contents")
                );
            }

        };
    }

 

👉 RowMapper란?

DB에서 select 쿼리한 결과(ResultSet)의 한 줄(row)

우리가 만든 Java 객체로 매핑해주는(변환해주는) 인터페이스

 

🧠 쉽게 바꿔 말하면 

SELECT * FROM memo;

을 실행하면 DB에서 여러 행(row)이 반환 됨.

 

RowMapper가 ResultSet 안의 한 줄(row)을 MemoResponseDto 객체로 바꿔줌. (즉, DB row → 자바 객체)

 

📦 DB의 각 행(row)을

MemoResponseDto(id, title, contents) 객체로 만들어주는 함수

 

ResultSet이란

ResultSetDB에서 select 쿼리를 실행한 결과자바 코드에서 읽을 수 있도록 만들어진 “표 형태의 데이터 구조”

 

💡 아주 쉬운 코드 예시

ResultSet rs = statement.executeQuery("SELECT * FROM memo");

while (rs.next()) { //rs.next() : 다음 행(row)으로 이동하는 것
    Long id = rs.getLong("id");
    String title = rs.getString("title"); //rs.getString("컬럼명") : 해당 셀의 값을 꺼내는 것
    String contents = rs.getString("contents"); 

    System.out.println(id + " / " + title + " / " + contents);
}

 

getTimestamp() vs getDate() vs getTime() 차이점

메서드 반환 타입 포함 정보 설명
getTimestamp() java.sql.Timestamp 날짜 + 시간 모두 포함 가장 일반적, 자주 씀
getTime() java.sql.Time ⏱️ 시간(HH:mm:ss) 만 포함 시:분:초만 필요한 경우
getDate() java.sql.Date 📅 날짜(yyyy-MM-dd) 만 포함 시:분:초는 무시됨

 

 


👾 Create Date를 DB에서 자동 생성하게 설정했으나, null이 들어가던 문제

✅ 핵심 원인

SimpleJdbcInsert는 자동으로 모든 필드에 대해 insert SQL을 생성해주는데,

테이블의 모든 컬럼 중에서 parameters에 안 넣은 필드도 무조건 null로 넣어버림 😱

 

기존 스케쥴 저장 Repository 코드

@Override
    public ScheduleResponseDto saveScheudle(Schedules sc) {
        // INSERT Query를 직접 작성하지 않아도 된다.
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.withTableName("schedules").usingGeneratedKeyColumns("id");

        Map<String, Object> parameters = new HashMap<>();
        parameters.put("title",sc.getTitle());
        parameters.put("content",sc.getContent());
        parameters.put("user_id",sc.getUserId()); //외래키로 유저테이블과 연결하기
        parameters.put("update_date",sc.getUpdate_date());

        // 저장 후 생성된 key값을 Number 타입으로 반환하는 메서드
        // executeAndReturnKey : DB에 insert 하고, AUTO_INCREMENT 또는 SERIAL 같은 자동 생성된 id 값을 반환해주는 메서드
        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));

        return new ScheduleResponseDto(key.longValue(), sc.getTitle(), sc.getContent(), sc.getUserId(),sc.getCreate_date(),sc.getUpdate_date());
    }

 

✨ [방법 1] SimpleJdbcInsert에 사용할 컬럼 명시하기

SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate)
    .withTableName("schedules")
    .usingGeneratedKeyColumns("id")
    .usingColumns("title", "content", "user_id", "update_date"); // 👈 create_date는 미등록

이렇게 하면 지정한 컬럼만 insert SQL에 포함되고

create_dateDB가 알아서 자동으로 넣어준다고 함.

 

✨ [방법 2] 직접 insert SQL을 작성

String sql = "INSERT INTO schedules (title, content, user_id, update_date) VALUES (?, ?, ?, ?)";
jdbcTemplate.update(sql, sc.getTitle(), sc.getContent(), sc.getUserId(), sc.getUpdate_date());

조건 중 한 가지만을 충족하거나, 둘 다 충족을 하지 않을 수도, 두 가지를 모두 충족하는 방법

@RequestParam(required = false) 어노테이션 추가로 해결

@GetMapping
public List<ScheduleResponseDto> findAllSchedules(
    @RequestParam(required = false) String date,  // 날짜는 문자열로 받아서 변환 추천
    @RequestParam(required = false) String name
) {
    return scService.findAllSchedules(date, name);
}

컨트롤러에서 위와 같이 수정 후 DB 불러오는 구조 수정 진행

여기서 동적 쿼리랑 SpringBuilder를 쓰는 방법을 알아야 함;;

(이건 안배웠는디유... 찾아서 알아냄... 과제가 어렵네욧 선생님...)

 

SpringBuilder는 쿼리를

👉 append() 메서드로 문자열을 계속 이어 붙이기 가능하고,

👉 마지막에 toString()으로 문자열로 변환가능한 메서드를 제공함!

 

SELECT * FROM users WHERE 1=1;

 

 1=1은 **항상 참(TRUE)**인 조건으로 (모든 레코드 선택 (= 조건 없음)을 의미)

SELECT * FROM users;

와 동일한 의미인데 WHERE 1=1를 붙이는 이유는 아래 예시 참고

 

💬 조건 없이 시작하면 불편한 경우

-- 사용자가 조건을 안 넣을 수도 있고 넣을 수도 있음
SELECT * FROM users
WHERE age > 20  -- 첫 조건이 있으면 ok

-- 근데 조건이 없으면 WHERE도 없어야 함
SELECT * FROM users  -- 이렇게...

-- 조건 여러 개면? 첫 조건엔 WHERE, 나머진 AND 붙여야 함
SELECT * FROM users
WHERE age > 20
AND gender = 'F'

→ 조건이 몇 개 있는지에 따라 WHERE vs AND 분기처리를 해야 함

 

✅ 그래서 WHERE 1=1을 써버리면!

SELECT * FROM users
WHERE 1=1
AND age > 20
AND gender = 'F'

→ 조건이 있든 없든 그냥 AND만 붙이면 됨

 

🔄 다른 연산자도 붙일 수는 있음!

SELECT * FROM users
WHERE 1=1
OR name = 'John'

이건 문법적으로는 되지만…

 

💥 문제 생김

1=1이 항상 참이니까 → OR name = 'John'이든 뭐든 상관없이 항상 모든 row가 나와.

OR보단 보통 AND를 쓰는 이유는:

👉 WHERE 1=1 뒤에 오는 조건들이 실제로 필터 역할을 하게 하려면 AND가 필요함!

 

그래서 최종

@Override
public List<ScheduleResponseDto> findAllSchedules(String date, String name) {
    StringBuilder sql = new StringBuilder("SELECT * FROM schedules WHERE 1=1");

    List<Object> params = new ArrayList<>();

    if (date != null) {
        sql.append(" AND DATE(update_date) = ?");
        params.add(date); // "2025-03-27"
    }

    if (name != null) {
        sql.append(" AND user_id IN (SELECT id FROM users WHERE name = ?)");
        params.add(name);
    }

    sql.append(" ORDER BY update_date DESC");

    return jdbcTemplate.query(sql.toString(), scheduleRowMapper(), params.toArray());
    //return jdbcTemplate.query(
    //sql.toString(),         // ① SQL 문장 (String)
    //scheduleRowMapper(),    // ② RowMapper : 결과를 객체로 매핑
    //params.toArray()        // ③ SQL의 ?에 들어갈 값들 (Object[]));
}

 

✅ 최종적으로 이 둘이 같이 동작해서 아래처럼 실행 됨

String sql = "SELECT * FROM schedules WHERE 1=1 AND DATE(update_date) = ? AND user_id IN (...) ORDER BY ...";
List<Object> params = List.of("2025-03-27", "sun");

jdbcTemplate.query(sql, rowMapper, params.toArray());

 

개발 전, 공통 조건

더보기
  • 일정 작성, 수정, 조회 시 반환 받은 일정 정보에 비밀번호는 제외해야 합니다.
  • 일정 수정, 삭제 시 선택한 일정의 비밀번호와 요청할 때 함께 보낸 비밀번호가 일치할 경우에만 가능합니다.
    • 비밀번호가 일치하지 않을 경우 적절한 오류 코드 및 메세지를 반환해야 합니다.
  • 3 Layer Architecture 에 따라 각 Layer의 목적에 맞게 개발해야 합니다.
  • CRUD 필수 기능은 모두 데이터베이스 연결 및 JDBC 를 사용해서 개발해야 합니다.
    • JDBC 설정은 강의와 강의에서 제공해주는 코드 스니펫을 참고하셔도 됩니다.
    • ‼️ 잠깐! 왜 JPA가 아닌 JDBC로 하나요?
      • 데이터베이스와의 연동을 위해 JDBC를 사용해보며, 기본적인 SQL 쿼리 작성과 데이터 관리를 연습합니다.
      • JPA에 비해, 첫 시도로 좋은 연습 상대가 될거에요! 충분히 익숙해지고 난 후, JPA를 도입합니다!

 

필수 기능 가이드

 

Lv 0 API 명세 및 ERD 작성

더보기

Lv 0. API 명세 및 ERD 작성 필수

  • [ ] API 명세서 작성하기
    • [ ] API명세서는 프로젝트 root(최상위) 경로의 README.md 에 작성
    • 참고) API 명세서 작성 가이드
      • API 명세서란 API명, 요청 값(파라미터), 반환 값, 인증/인가 방식, 데이터 및 전달 형식 등 API를 정확하게 호출하고 그 결과를 명확하게 해석하는데 필요한 정보들을 일관된 형식으로 기술한 것을 의미합니다.
      • request 및 response는 json(링크) 형태로 작성합니다.
      예) [서점] 책 API 설계하기

 

  • [ ] ERD 작성하기
    • [ ] ERD는 프로젝트 root(최상위) 경로의 README.md 에 첨부
  • 참고) ERD 작성 가이드출처: https://online.visual-paradigm.com/ko/community/share/er-diagram-for-online-book-store-1gnrscfbme
    • API 명세 작성을 통해 서비스의 큰 흐름과 기능을 파악 하셨다면 이제는 기능을 구현하기 위해 필요한 데이터가 무엇인지 생각해봐야합니다.
      • 이때, 구현해야 할 서비스의 영역별로 필요한 데이터를 설계하고 각 영역간의 관계를 표현하는 방법이 있는데 이를 ERD(Entity Relationship Diagram)라 부릅니다.
    • ERD 작성간에 다음과 같은 항목들을 학습합니다.
      • E(Entity. 개체)
        • 구현 할 서비스의 영역에서 필요로 하는 데이터를 담을 개체를 의미합니다.
          • ex) 책, 저자, 독자, 리뷰
      • A(Attribute. 속성)
        • 각 개체가 가지는 속성을 의미합니다.
          • ex) 책은 제목, 언어, 출판일, 저자, 가격 등의 속성을 가질 수 있습니다.
      • R(Relationship. 관계)
        • 개체들 사이의 관계를 정의합니다.
          • ex) 저자는 여러 권의 책을 집필할 수 있습니다. 이때, 저자와 책의 관계는 일대다(1:N) 관계입니다.
    ERD 추천 무료 Tool ERDCloud
  • ERD 추천 영상 https://www.youtube.com/watch?v=jsOPr3QfMW0
  • [ ] SQL 작성하기
    • [ ] 설치한 데이터베이스(Mysql)에 ERD를 따라 테이블을 생성
    • 참고) SQL 작성 가이드
      • 과제 프로그램의 root(최상위) 경로에schedule.sql 파일을 만들고, 테이블 생성에 필요한 query를 작성하세요.

Lv 1. 일정 생성 및 조회

더보기
  • [ ] 일정 생성(일정 작성하기)
    • [ ] 일정 생성 시, 포함되어야할 데이터
      • [ ] 할일, 작성자명, 비밀번호, 작성/수정일을 저장
      • [ ] 작성/수정일은 날짜와 시간을 모두 포함한 형태
    • [ ] 각 일정의 고유 식별자(ID)를 자동으로 생성하여 관리
    • [ ] 최초 입력 시, 수정일은 작성일과 동일
    • View를 생각해보자면..! (화면 구현하는 것은 요구사항이 아닙니다!)
  • [ ] 전체 일정 조회(등록된 일정 불러오기)
    • [ ] 다음 조건을 바탕으로 등록된 일정 목록을 전부 조회
      • [ ] 수정일 (형식 : YYYY-MM-DD)
      • [ ] 작성자명
    • [ ] 조건 중 한 가지만을 충족하거나, 둘 다 충족을 하지 않을 수도, 두 가지를 모두 충족할 수도 있습니다.
    • [ ] 수정일 기준 내림차순으로 정렬하여 조회
    • View를 생각해보자면…! (화면 구현하는 것은 요구사항이 아닙니다!)
  • [ ] 선택 일정 조회(선택한 일정 정보 불러오기)
    • [ ] 선택한 일정 단건의 정보를 조회할 수 있습니다.
    • [ ] 일정의 고유 식별자(ID)를 사용하여 조회합니다.
      •  

 

Lv 2. 일정 수정 및 삭제

더보기
  • [ ] 선택한 일정 수정
    • [ ] 선택한 일정 내용 중 할일, 작성자명 만 수정 가능
      • [ ] 서버에 일정 수정을 요청할 때 비밀번호를 함께 전달합니다.
      • [ ] 작성일 은 변경할 수 없으며, 수정일 은 수정 완료 시, 수정한 시점으로 변경합니다.
  • [ ] 선택한 일정 삭제
    • [ ] 선택한 일정을 삭제할 수 있습니다.
      • [ ] 서버에 일정 수정을 요청할 때 비밀번호를 함께 전달합니다.

 

Lv 3. 연관 관계 설정

더보기
  • [ ] 작성자와 일정의 연결
    • [ ] 설명
      • [ ] 동명이인의 작성자가 있어 어떤 작성자가 등록한 ‘할 일’인지 구별할 수 없음
      • [ ] 작성자를 할 일과 분리해서 관리합니다.
      • [ ] 작성자 테이블을 생성하고 일정 테이블에 FK를 생성해 연관관계를 설정해 봅니다.
    • [ ] 조건
      • [ ] 작성자 테이블은 이름 외에 이메일, 등록일, 수정일 정보를 가지고 있습니다.
        • [ ] 작성자의 정보는 추가로 받을 수 있습니다.(조건만 만족한다면 다른 데이터 추가 가능)
      • [ ] 작성자의 고유 식별자를 통해 일정이 검색이 될 수 있도록 전체 일정 조회 코드 수정.
      • [ ] 작성자의 고유 식별자가 일정 테이블의 외래키가 될 수 있도록 합니다.

 

Lv 4. 페이지네이션

더보기
  • [ ] 설명
    • [ ] 많은 양의 데이터를 효율적으로 표시하기 위해 데이터를 여러 페이지로 나눕니다.
      • [ ] 페이지 번호와 페이지 크기를 쿼리 파라미터로 전달하여 요청하는 항목을 나타냅니다.
      • [ ] 전달받은 페이지 번호와 크기를 기준으로 쿼리를 작성하여 필요한 데이터만을 조회하고 반환
  • [ ] 조건
    • [ ] 등록된 일정 목록을 페이지 번호와 크기를 기준으로 모두 조회
    • [ ] 조회한 일정 목록에는 작성자 이름이 포함
    • [ ] 범위를 넘어선 페이지를 요청하는 경우 빈 배열을 반환
    • [ ] Paging 객체를 활용할 수 있음

 

Lv 5. 예외 발생 처리

더보기
  • [ ] 설명
    • [ ] 예외 상황에 대한 처리를 위해 HTTP 상태 코드(링크)와 에러 메시지를 포함한 정보를 사용하여 예외를 관리할 수 있습니다.
      1. 필요에 따라 사용자 정의 예외 클래스를 생성하여 예외 처리를 수행할 수 있습니다.
      2. @ExceptionHandler를 활용하여 공통 예외 처리를 구현할 수도 있습니다.
      3. 예외가 발생할 경우 적절한 HTTP 상태 코드와 함께 사용자에게 메시지를 전달하여 상황을 관리합니다.
  • [ ] 조건
    • [ ] 수정, 삭제 시 요청할 때 보내는 비밀번호가 일치하지 않을 때 예외가 발생합니다.
    • [ ] 선택한 일정 정보를 조회할 수 없을 때 예외가 발생합니다.
      1. 잘못된 정보로 조회하려고 할 때
      2. 이미 삭제된 정보를 조회하려고 할 때

 

Lv 6. null 체크 및 특정 패턴에 대한 검증 수행

더보기
  • [ ] 설명
    • [ ] 유효성 검사
      1. 잘못된 입력이나 요청을 미리 방지할 수 있습니다.
      2. 데이터의 무결성을 보장하고 애플리케이션의 예측 가능성을 높여줍니다.
      3. Spring에서 제공하는 @Valid 어노테이션을 이용할 수 있습니다.
  • [ ] 조건
    • [ ] 할일은 최대 200자 이내로 제한, 필수값 처리
    • [ ] 비밀번호는 필수값 처리
    • [ ] 담당자의 이메일 정보가 형식에 맞는지 확인

 

Why: 과제 제출시에는 아래 질문을 고민해보고 답변을 함께 제출해주세요.

  1. 적절한 관심사 분리를 적용하셨나요? (Controller, Service, Repository)
  2. RESTful한 API를 설계하셨나요? 어떤 부분이 그런가요? 어떤 부분이 그렇지 않나요?
  3. 수정, 삭제 API의 request를 어떤 방식으로 사용 하셨나요? (param, query, body)

제출 일정

더보기

최종 제출

  • 제출 기한 : 3/26(수) 14:00 까지
  • 제출해야 할 것
    1. 과제
      1. Github 링크 (Public)
      2. 트러블슈팅을 작성한 TIL
      3. 어디까지 구현했는지
      4. 어떤 부분을 고민했는지, 어려웠던 부분은 어디였는지
        1. 해당 부분을 작성하시면 피드백 간, 튜터님께서 확인 후 피드백 해주실 예정입니다.
      5. 고민해볼 사항
        1. 적절한 관심사 분리를 적용하셨나요? (Controller, Service, Repository)
        2. RESTful한 API를 설계하셨나요? 어떤 부분이 그런가요? 어떤 부분이 그렇지 않나요?
        3. 수정, 삭제 API의 request를 어떤 방식으로 사용 하셨나요? (param, query, body)