Recording/멋쟁이사자처럼 BE 13기

[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_40일차_"Spring JDBC"

LEFT 2025. 2. 4. 17:48

🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [40]일차

🚀40차에는 데이터베이스와의 접근을 단순화하여 도와주는 스프링 프레임워크의 Spring JDBC를 배울 수 있었다.

자바의 JDBC에서 복잡했던 과정을 간소화하여 도와주는 기술로 데이터베이스 CRUD 연산을 조금 더 익숙하게 실습할 수 있었다.


Spring 프레임워크 에러페이지

  • 스프링 프레임워크에서 오류가 발생할때 (ex. 지정되지 않은 URL의 페이지 요청 시 등)
    출력되는 [White Label Page] 에러 페이지WAS가 만든 에러페이지인 것
  • 이러한 에러페이지를 사용자가 직접 만들 수도 있다.
    (WAS의 에러페이지가 아닌 스프링이 에러페이지를 만들도록 기능을 제공)

templates/error.html

<body>
	<h1> 사용자 정의 Error Page </h1>
	<p th:text="${errorMessage}"></p>
</body>

 

GlobalExceptionHandler 클래스

@ControllerAdvice
public class GlobalExceptionHandler {
		//...
    @ExceptionHandler(Exception.class)
		public ModelAndView handlerException(Exception e){
        //...
        return new ModelAndView("error");
    }
}
  • 에러메시지를 출력할 클래스에는 ControllerAdvice를 설정해주어야하는데  
    @ControllerAdvice 처럼 어노테이션을 적어주어야한다.

  • GlobalExceptionHandler : 모든 예외처리를 담당할 핸들러라는 이름으로 임의 지정
  • @ControllerAdvice로 사용자 정의 예외처리 클래스를 만들었다면
    application.yml 설정파일에도 이 예외처리를 사용하겠다는 설정을 넣어주어야한다.

  • 이 설정은 "서버 자체적으로 보여주는 whitelabel(에러페이지)"를 비활성화시켜서
    사용자가 정의한 에러페이지로 출력할 수 있도록 함

@ExceptionHandler(Exception.class)
public ModelAndView handlerException(Exception e, Model model){
    System.out.println("error ::::: " + e);
    model.addAttribute("errorMessage", e.getMessage());
    return new ModelAndView("error");
}
  • @ExceptionHandler(Exception.class)
    ➡️모든 Exception들을 여기서 처리한다는 의미
    어노테이션에 붙은 이 Exception.class는 “어느 범위까지의 Exception을 처리할지 정의”
    ex. @ExceptionHandler(RuntimeException.class)
    ex. @ExceptionHandler(MyBankException.class) 사용자 정의 예외처리처럼 예외를 처리할 범위를 지정하는 것

  • 반면 handlerException(Exception e) 메소드의 Exception은 예외가 발생했을때의 처리를 위해
    매개변수로 Exception e 를 가져오는 것이다.
    이로써 e.getMessage()와 같은 문구로 예외발생의 종류를 출력해볼 수 있는 것으로 활용

  • mode 객체에 errorMessage라는 이름으로 접근 시 e.getMessage()가 출력되도록 함
    ➡️사용자 정의 예외처리가 발생 시 로그 부분에 사용자 정의 예외처리가 출력
    (ex. 올바르지 않은 문자입니다, 정해진 숫자의 범위 내에서 입력해주세요 등)

  • localhost:8080/test 처럼 지정되지 않은 페이지나 만들어지지 않은 페이지를 요청했을때
    오류 페이지가 직접 만든 에러페이지로 출력될 것

LoggerFactory

private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(Exception.class)
public ModelAndView handlerException(Exception e, Model model){
		logger.error("Server Error", e);
		
    System.out.println("error ::::: " + e);
    model.addAttribute("errorMessage", e.getMessage());
    return new ModelAndView("error");
}
  • LoggerFactory.getLoger(GlobalExceptionHandler.class)
    ➡️Logger 추가
    GlobalExceptionHandler.class 현재 클래스를 대상으로 getLogger() (=로거를 만들겠다)는 의미

  • System.out.println(”error ::::: “ + e);
    실질적으로는 이처럼 시스템을 직접 건드리는 코드는 남겨두지 않아야하고
    logger.error(”server Eeror”, e)처럼 Logger 객체를 얻어와서 사용하는 것을 권장

  • Logger객체를 사용하면 localhost:8080/test 페이지를 요청했을때
    로그 창에 지정해놨던 에러 메시지가 출력됨을 확인 가능

레이어드 아키텍처

  • 소프트웨어 설계에서 널리 사용되는 패턴 중 하나
  • 애플리케이션을 명확하게 분리된 여러 계층으로 구조화하는 방식
  • 주요 목적 : 관심사의 분리를 통해 애플리케이션의 유지보수성, 확장성 및 개발효율성 향상

  • 레이어드 아키텍처에서 Transactions (트랜잭션)이 Service Layer까지 걸쳐있는데
    이는 트랜잭션을 Service Layer에서 처리해야한다는 것

  • 트랜잭션은 기본적으로 데이터베이스와 연결되어있기에
    Persistence Layer인 Repository에서 처리해야한다고 생각할 수 있지만 실제로는 Service Layer에서 처리한다는 것

트랜잭션

  • 나눌 수 없는 업무의 한 단위
  • ex. 계좌이체라는 “비즈니스”는 Service Layer에서 일어난다.
    이 비즈니스는 (A계좌 입금 + B계좌 출금)이 함께 일어나는데
    이는 Persistence Layer인 Repository에서 (입금 한번, 출금 한번)이 수행된다.

  • Service Layer에는 (입금 한번, 출금 한번)이 모두 완료된 비즈니스인 “계좌이체”라는 행위가
    모두 성공적으로 일어나야 “커밋”
    그렇지 않으면 “롤백”을 하는 것이다.

  • 정리하자면 실제 (입금, 출금)은 Repository에서 CRUD 연산을 수행하는 것이고
    계좌이체라는 행위자체는 Service Layer에서 관리하는 것이다.

  • 따라서 트랜잭션을 처리하는 것은 Service Layer에서 담당한다고 볼 수 있다.

레이어드 아키텍처 계층 

  • 프레젠테이션 계층
    ➡️사용자 인터페이스와 직접적으로 상호작용
    ex. 웹 애플리케이션에서의 컨트롤러와 뷰

  • 비즈니스 로직 계층 or 서비스 계층
    ➡️애플리케이션의 핵심 기능과 비즈니스 규칙 처리
    ➡️ 프레젠테이션 계층과 데이터 접근 계층 사이에서의 중재자 역할
    ex. 사용자 인증, 데이터처리 로직 등을 수행하는 서비스 컴포넌트

  • 데이터 접근 계층
    ➡️데이터베이스나 파일시스템과 같은 데이터 소스를 직접 관리
    ➡️ CRUD 작업을 추상화하여 비즈니스 로직 계층이 데이터베이스 구조에 의존하지 않도록 함
    ex. Repository나 DAO(Data Access Object) 패턴

계층 별 역할

  • Controller
    ➡️사용자의 요청을 받아 비즈니스 로직을 호출하고 결과를 뷰에 전달하는 역할
    ➡️ 웹 요청과 응답 처리, HTTP 요청 파싱 및 라우팅
    ex. Spring MVC의 @Controller 또는 @RestController

  • Service
    ➡️실제 비즈니스 로직을 수행 (여러 데이터 모델 조합 및 복잡한 계산에 따른 작업 실행)
    ➡️ 하나의 메소드는 하나의 비즈니스 기능을 수행하도록 설계
    트랜잭션관리도 이 계층에서 주로 처리

    ➡️ Spring에서는 @Service 어노테이션으로 선언 후 @Transactional 어노테이션에서 트랜잭션을 처리
    (=데이터베이스 트랜잭션을 관리)한다.

  • Repository
    ➡️데이터베이스와의 모든 상호작용을 캡슐화하고 데이터베이스 쿼리를 직접 관리
    ➡️ 데이터를 객체로 매핑하는 ORM(Object-Relational Mapping) 프레임워크와 함께 사용
    ➡️ 데이터 지속성 관리

    ex. 이 계층에서는 Spring JDBC, MyBatis, JPA 가 가장 많이 사용
    이 들은 데이터를 꺼내고 넣을때를 편하게 도와주는 도구

    ➡️Spring JDBC와 MyBatis는 사용자가 주는 쿼리로 자동으로 수행한다는 점에서 비슷하다.
    ➡️반면 JPA는 ORM 방식을 사용하여 객체와 관계형 DB의 테이블을 매핑시켜주면
    알아서 쿼리를 만들고 동작하게 해주는 프레임워크
    이처럼 JPA는 내부적으로 동작하는 것이 Spring JDBC와 MyBatis와는 다르다.

Spring 프레임워크는 이 Spring JDBC, MyBatis, JPA 등의 사용방법을 통일시켜주는 표준을 제공해주는데
그 과정을 Spring Data JDBC에서 제공한다.

레이어드 아키텍처의 계층 분리의 장점으로
➡️각 계층이 서로 독립적으로 개발, 테스트, 유지보수될 수 있고
➡️각 계층은 오직 인접한 계층과만 통신한다.
➡️상위 계층은 하위 계층에 의존 (=낮은 결합도와 높은 응집도)
➡️소프트웨어 개발 원칙인 SOLID를 준수할 수 있음


Spring Data JDBC

  • Spring Data 프로젝트의 일부 (Spring JDBC, MyBatis, JPA의 사용방법을 표준화(통일)시켜주는 프로젝트)
  • 데이터 접근 계층을 단순화하는 목적으로 설계됨
  • Spring Data JDBC를 사용하면 JDBC를 이용해 DB와 상호작용할때의 반복적 코드를 줄일 수 있음

  • 도메인 모델을 중심으로 작동하고 Repository 추상화로 DB 연산을 직관적으로 수행 가능
    ➡️도메인 중심 설계 : 도메인 객체와 DB 테이블 간의 매핑을 간편하게 설정하는 것으로
    도메인 로직과 데이터베이스 로직의 분리가 명확해져 도메인 주도 설계 (DDD)를 적용하기에 용이

  • 개발자는 인터페이스만 정의하면 Spring Data JDBC가 실행 시 구현체를 자동으로 생성한다.

  • 쿼리 메소드 : 메소드 이름만으로 쿼리를 생성하는 기능 제공
    복잡한 쿼리의 경우 @Query 어노테이션을 사용하여 SQL을 직접 정의 가능

  • Spring Data JDBC 사용은 대부분 설정이 자동처리되지만 의존성과 DB 연결 설정 부분은 직접 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'mysql:mysql-connector-java:8.0.23'
  • 👀JPA는 Hibernate라는 기술로부터 비롯되었으므로 JPA의 구현체는 Hibernate로 되어있다.
    즉 자바에서 Hibernate를 표준으로 뽑아낸 것이 JPA라고 볼 수 있다.

❓JDBC vs Spring JDBC

JDBC (Java Database Connetivity)

  • 자바에서 데이터베이스에 접근할 수 있게 해주는 API
  • SQL쿼리 실행, DB 연결 관리 등 기능 제공
  • 데이터베이스와 직접적인 상호작용 처리

Spring JDBC

  • Spring 프레임워크의 일부
  • JDBC의 복잡성을 추상화하고 간소화
  • 템플릿 디자인 패턴을 사용하여 코드 중복을 줄이고 개발자가 SQL쿼리에 집중할 수 있도록 함
    (JdbcTemplate과 같은 템플릿 클래스 사용)

  • 예외처리를 일관된 방식으로 처리할 수 있게하여 DB작업을 효율적이고 안정적으로 만듦
    (스프링의 DataAccessException 으로 일관되게 처리)

  • 리소스 관리 : DB 연결 같은 리소스를 자동으로 관리하여 사용 후 자동으로 close해서 리소스 누수를 방지

Spring JDBC

Spring JDBC의 구조

  • DataSource
    ➡️모든 Spring JDBC 작업의 기반으로 DB 연결을 관리
    ➡️Spring Boot에서는 application.yml 파일을 통해 DataSource를 쉽게 설정 가능
    ➡️Connection Pool (커넥션 풀)을 관리하는 것이 주요 업무
    (DB 접속에 매번 접속하게되면 비용이 많이 들 수 있어 커넥션 풀에서 접속상태를 유지하다가 연결하도록 함)

  • JdbcTemplate
    ➡️SQL쿼리 실행을 단순화하는 주요 클래스, JDBC 작업 시 필요한 많은 세부사항 처리
    ex. DB 연결을 열고, PreparedStatement를 생성하고, 실행, 결과 매핑, 모든 리소스 자동 정리 등

  • NamedParameterJdbcTemplate
    ➡️JdbcTemplate의 확장으로 쿼리에서 이름 기반의 파라미터를 사용해 쿼리 가독성과 유지보수성을 높임

  • SimpleJdbcInsert 와 SimpleJdbcCall
    ➡️데이터 삽입과 스토어드 프로시저 호출을 위한 편리한 방법 제공

JdbcTemplate 클래스

  • query(), update(), execute() 등이 주요 메소드
  • CRUD연산을 수행함
  • CREATE - update()메소드 사용
  • READ - queryForObject() 또는 query() 메소드 사용
  • UPDATE - update() 메소드 사용
  • DELETE - update() 메소드 사용

Spring JDBC 설정 추가

spring:
  application:
    name: springjdbc

  datasource:
    url: jdbc:mysql://localhost:3306/springdb
    username: like
    password: lion
    driver-class-name: com.mysql.cj.jdbc.Driver
  •  application.yml에서 설정을 추가해준다.
  • 💡아이디, 패스워드 등은 Spring JDBC에서 자동으로 해줄 수 없으므로
    사용자가 직접 설정파일에 추가해주어야 함

Application에서 CommandLineRunner 구현

@SpringBootApplication
public class SpringJDBCApplication01 implements CommandLineRunner{
    public static void main(String[] args) {
        SpringApplication.run(SpringJDBCApplication01.class);
    }
    
    @Override
    public void run(String... args) throws Exception {
    	//테스트할 부분 입력
    }
}
  • implements CommandLineRunner
    ➡️간단한 테스트를 위한 run()메소드를 가진 인터페이스

  • Spring JDBC에서 자동으로 해주지 못하는 쿼리문 작성 같은 경우
    (// 테스트할 부분)에 사용자가 직접 입력해주어야함

JdbcTemplate 사용 추가

// 간단히 테스트할 것이므로 필드를 통한 주입 방법 채택
@Autowired
private JdbcTemplate jdbcTemplate;
  • @Autowired 추가
    ➡️DB입력, 수정, 조회 등을 간편하게 도와주는 JdbcTemplate 클래스 선언
  • 간단히 테스트할 것이므로 필드를 통한 주입 방법으로 적용 (그 외 설정자 주입방법, 생성자 주입방법(권장) 등)

Spring JDBC - 입력 (CREATE)

@SpringBootApplication
public class SpringJDBCApplication01 implements CommandLineRunner {
    public static void main(String[] args) {
        SpringApplication.run(SpringJDBCApplication01.class);
    }

    // 간단히 테스트할 것이므로 필드를 통한 주입 방법 채택
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public void run(String... args) throws Exception {
        // Spring JDBC가 대신해주지 못하는 부분 넣기
        String sql = "INSERT INTO users(name,email) VALUES(?,?)"; // users테이블에 name, email값 넣기
        jdbcTemplate.update(sql, "Watkins", "Watkins@premier.com");
    }
}

int count = jdbcTemplate.update(sql, "Watkins", "Watkins@premier.com");
System.out.println("데이터베이스에 적용한 횟수 : " + count);

  • count를 추가하면 적용이 잘 되었는지 로그창에서 바로 확인해볼 수도 있다.

Spring JDBC - 조회 (SELECT)

// 1. 조회 (SELECT) - SQL문 별도로 작성
String sql2 = "SELECT id,name,email FROM users";
jdbcTemplate.query(sql2, 결과값을 담을 공간);
// 2. 조회 (SELECT) - SQL문 내부에 작성
jdbcTemplate.query("SELECT id,name,email FROM users", 결과값을 담을 공간);
  •  sql을 따로 작성 후 query()메소드의 인자로 넣어주어도되고, 직접 넣어주어도된다.
  • Spring JDBC에서 조회한 쿼리의 결과를 담아주어야하는데 그것을 두번째 인자에 담아주어야한다.

쿼리 결과를 담을 규격 - User 클래스

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User {
    private Long id;
    private String name;
    private String email;
}
  • Lombok을 활용하여
    - Getter, Setter 메소드 (@Getter, @Setter)
    - 모든 id,name,email을 담아서 생성하는 생성자 (@AllArgsConstructor)
    - 아무것도 없는 기본생성자 (@NoArgsConstructor)
    - ToString()메소드를 적용 (@ToString) 

  • @Setter는 데이터베이스 테이블에서 가져온 값을 User클래스에 적용하기 위함이다.
    ➡️Setter를 적용해주지 않으면 값을 불러왔을때 null이 발생할 것

  • 이 User클래스의 규격에 맞게 실제 결과 값을 담는 객체로 RowMapper가 있는데 스프링 프레임워크가 제공한다.

 

jdbcTemplate.query("SELECT id,name,email FROM users", new BeanPropertyRowMapper<>(User.class));
  • new를 통해 Spring JDBC가 기본으로 제공해주는 BeanPropertyRowMapper 객체를 사용

  • 이때 query()메소드는 List형태로 리턴하므로
// 조회 (SELECT)
List<User> users = jdbcTemplate.query("SELECT id,name,email FROM users", new BeanPropertyRowMapper<>(User.class));
  • 이처럼 List<User>타입의 users에 넣어서 ForEach문으로 확인가능


BeanPropertyRowMapper

  • Spring JDBC가 기본으로 제공해주는 RowMapper
  • User라는 클래스의 필드가 데이터베이스의 users 테이블의 컬럼이
    ➡️완전히 일치할 경우 이 기능을 사용하여 값을 담을 수 있다.
    ➡️완전히는 일치하지 않을 경우 RowMapper클래스를 새롭게 정의하여 사용할 수 있다.

[User클래스 필드명] 와 [데이터베이스의 컬럼명]이 일치하지 않을 경우 

RowMapper<User> rowMapper = new RowMapper<User>() {
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new User(
                rs.getLong("id"),
                rs.getString("name"),
                rs.getString("email")
        );
    }
};
  • new BeanPropertyRowMapper<>(User.class) 처럼 RowMapper()를 넣어주는 것 대신에
    RowMapper를 새롭게 정의하여 return 부에서 rs.getLong(”id”)… 처럼 각 컬럼들을 가져올 수 있다

  • 완전히 일치하지 않는 다는 것의 예시
    ex. name을 다루는 데이터가
    데이터베이스 컬럼에서 user_name
    User클래스의 필드에서 name
    으로 지정되어있어 일치하지 않을 수 있다.

  • 이는 데이터베이스와 자바의 표기법이 다를 수 있기때문인데
    ➡️데이터베이스는 (_)언더바를 사용하는 스네이크 표기법, 자바에서는 카멜 표기법 등을 사용하기때문이다.

// 1. return부에 바로 사용하는 방법
return new User(
        rs.getLong("id"),
        rs.getString("name"),
        rs.getString("email")
);
// 2. 별도로 정의하는 방법
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
  • 이 둘은 같은 기능을 한다.
  • return부에서 바로 사용하는 방법은 RowMapper가 <User>타입으로 가져왔기때문에 바로 Getter메소드를 통해
    값을 가져올 수 있는 것이다.

 

RowMapper 객체 정의 후 두번째 인자로 넣기

// 조회 (SELECT)
List<User> users = jdbcTemplate.query("SELECT id,name,email FROM users", rowMapper);

▶️실습 - User클래스의 연산을 다룰 인터페이스 정의 (UserDao)

public interface UserDao {
    void insertUser(User user);
    List<User> findAllUsers();
    void updateUserEmail(String name, String email);
    void deleteUser(String name);
}
  • 기능할 메소드들을 미리 UserDao 인터페이스에 정의 가능

UserDaoImpl 클래스 - UserDao 인터페이스의 구현체

public class UserDaoImpl implements UserDao{
    private JdbcTemplate jdbcTemplate;

    public UserDaoImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    
    //... 오버라이딩된 메소드들
}
  • JdbcTemplate 객체 사용을 선언하는데 다른 필드들은 제외하고 이 JdbcTemplate 객체만 생성자를 만들고 싶다면
@RequiredArgsConstructor
public class UserDaoImpl implements UserDao{
    private final JdbcTemplate jdbcTemplate; // final이 붙은 필드
    // private String etc; // final이 붙지 않은 필드
    
    //... 오버라이딩된 메소드들
}
  • 이처럼 주입받아야하는 (Injection받아야하는)것들은 final 키워드를 붙여준 후
    클래스에 @RequiredArgsConstructor 어노테이션을 추가하면

  • final이라는 키워드가 붙은 필드들만 생성자를 추가할 수 있게 해준다

❓@RequiredArgsConstructor을 사용하는 이유

  • @RequiredArgsConstructor는 Lombok 라이브러리에서 제공하는 어노테이션
  • final로 선언된 필드 또는 @NonNull이 붙은 필드만을 포함하는 생성자(Constructor)를 자동으로 생성

가장 큰 목적은 의존성 주입을 간편하게 하기 위함이다.

스프링에서는 권장되는 생성자 주입 방식은 매번 생성자를 작성하는 것이 번거로울 수 있다.
이때 final 키워드가 붙은 필드는 불변성(immutable)을 가지므로,
final 키워드가 붙은 필드만 초기화 후 변경할 수 없게 하여 코드의 안정성을 높일 수 있는 것이다.

➡️
final 필드는 반드시 초기화가 필요함 (이를 통해 객체 상태가 일관되게 유지됨)


▶️실습 - UserDaoImpl 클래스 작성

@RequiredArgsConstructor
@Repository
public class UserDaoImpl implements UserDao{
    private final JdbcTemplate jdbcTemplate; // final이 붙은 필드
    // private String etc; // final이 붙지 않은 필드

    @Override
    public void insertUser(User user) {
        // 입력 (CREATE)
        String sql = "INSERT INTO users(name,email) VALUES(?,?)"; // users테이블에 name, email값 넣기
        int count = jdbcTemplate.update(sql, user.getName(), user.getEmail());
        System.out.println("데이터베이스에 적용한 횟수 : " + count);
    }

    @Override
    public List<User> findAllUsers() {
        return jdbcTemplate.query("SELECT * FROM users", new BeanPropertyRowMapper<>(User.class));
    }

    @Override
    public void updateUserEmail(String name, String email) {
        String sql = "UPDATE users SET email=? WHERE name=?";
        jdbcTemplate.update(sql, email, name); // 위에 정의된 쿼리문의 순서대로 email, name순으로 위치
    }

    @Override
    public void deleteUser(String name) {
        jdbcTemplate.update("DELETE FROM users WHERE name=?",name);
    }
}
  • @Repository 어노테이션을 추가하여 스프링에서 사용할 수 있는 컴포넌트임을 명시해줌
    (@SpringBootApplication이 @ComponentScan을 할때 그 스캔의 대상이 되기위함)

  • SQL문 작성 시 PreparedStatement를 사용하여 값이 들어오는 부분에 ( ? ) 를 활용
    ➡️PreparedStatement는 ( ? )에 값을 바인딩해주는 메소드를 제공한다.
    바인딩되는 값의 순서는 중요함

  • jdbcTemplate.update(sql, user.getName(), userEmail());
    ➡️바인딩되는 값은 User 클래스에서 Getter메소드를 통해 가져온다.

 

@SpringBootApplication
public class SpringJDBCApplication02 implements CommandLineRunner {
    public static void main(String[] args) {
        SpringApplication.run(SpringJDBCApplication02.class);
    }

    @Autowired
    private UserDao userDao;

    @Override
    public void run(String... args) throws Exception {
        //insert test
        userDao.insertUser(new User(null, "Baines", "Baines@premier.com"));
        
        //.. 다른 테스트들
    }
}
  •  @Autowired에 UserDao 객체를 필드로 사용하겠다고 명시함

❓@Autowired로 UserDao를 사용한 이유

➡️생성자를 통한 주입 방법이 더 권장되지만 @Autowired를 통해 스프링 프레임워크가 의존성을 자동으로 주입해주어
간단하고 빠른 테스트를 위해서는이러한 필드 주입 방식을 사용한다.

만약 생성자 주입 방식으로 코드를 수정하게되면 

@SpringBootApplication
public class SpringJDBCApplication02 implements CommandLineRunner {

    private final UserDao userDao;

    // 생성자 주입 (권장 방식)
    public SpringJDBCApplication02(UserDao userDao) {
        this.userDao = userDao;
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringJDBCApplication02.class);
    }

    @Override
    public void run(String... args) {
        userDao.insertUser(new User(null, "Baines", "Baines@premier.com"));
    }
}
  • @Autowired 없이도 스프링이 UserDao Bean을 자동으로 주입하며
  • final 키워드로 불변성 유지할 수 있다.
  • 또한 의존성이 명확하게 드러나 테스트 및 유지보수가 용이하다.

▶️실습 - User Dao의 CRUD 연산 

@Override
public void run(String... args) throws Exception {
    //insert test
    userDao.insertUser(new User(null, "Baines", "Baines@premier.com"));

    //update test
    userDao.updateUserEmail("Baines", "Everton@premier.com");

    // delete test
    userDao.deleteUser("Baines");

    //select test
    List<User> users = userDao.findAllUsers();
    for(User user : users){
        System.out.println(user);
    }
}

 

▶️실습 - INSERT

//insert test
userDao.insertUser(new User(null, "Baines", "Baines@premier.com"));

  • 데이터 입력 테스트를 먼저 진행해보면 7번 id로 제대로 들어간것을 확인할 수 있다.

 

▶️실습 - UPDATE

//update test
userDao.updateUserEmail("Baines", "Everton@premier.com");

  • 데이터 수정 테스트를 해보면 Baines@premier.com → Everton@premier.com 으로 제대로 바뀌었다.

▶️실습 - DELETE

// delete test
userDao.deleteUser("Baines");

  • 데이터 삭제 테스트를 해보면 name=Baines인 데이터가 삭제된 것을 확인할 수 있다.

▶️실습 - SELECT

//select test
List<User> users = userDao.findAllUsers();
for(User user : users){
    System.out.println(user);
}

  • 샘플데이터들을 더 추가해보고 데이터 조회 테스트를 해보면 로그 창에 잘 출력되는 것을 확인할 수 있다.

▶️실습 - Dept Dao

🚀오류 해결 과정

@Autowired
private UserDao userDao;

@Autowired // 꼭 각 필드마다 어노테이션을 붙여주어야한다.
private DeptDao deptDao;
  • 이처럼 각 필드마다 어노테이션을 꼭 붙여주어야하는 것을
@Autowired
private UserDao userDao;
private DeptDao deptDao;
  • 이처럼 어노테이션을 붙여주지 않고 바로 UserDao의 밑에 써주게되어 생긴 문제가 있었다.
    ➡️Dept 클래스를 찾지 못하는 문제가 발생하였다.

  • 별도로 @Autowired 어노테이션을 붙여주는 것으로 해결할 수 있었다.

CRUD 연산에 예외처리 추가

@Override
public void insertDept(Dept dept) {
    String sql = "INSERT INTO dept(dept_name,location) VALUES(?,?)";
    try{
        jdbcTemplate.update(sql, dept.getDeptName(), dept.getLocation());
    }catch(DataAccessException e){
        throw new DataAccessException("사용자가 잘못된 값을 입력하였습니다. " + dept.getDeptName(), e){};
    }
}
  • ⭐DataAccessException은 Spring프레임워크의 dao에서 제공하고 있는 Exception

 

@Override
public List<Dept> findAllDept() {
    try{
        return jdbcTemplate.query("SELECT * FROM dept", rowMapper);
    }catch(DataAccessException e){
        throw new DataAccessException("찾으려는 데이터가 없습니다.", e) {};
    }
}
  • dept테이블을 조회하는 메소드 부분에서도 예외처리를 추가할 수 있다.

 

public class DeptDataNotFoundException extends RuntimeException{
    public DeptDataNotFoundException(String message){
        super(message);
    }
}
  • 이처럼 사용자 정의 예외를 따로 만들어 적용해줄 수도 있다.

 

@Override
public void updateDept(Dept dept) {
    String sql = "UPDATE dept SET dept_name=?, location=? where dept_no=?";
    int updated = jdbcTemplate.update(sql, dept.getDeptName(), dept.getLocation(), dept.getId());

    if(updated == 0){
        throw new DeptDataNotFoundException("수정할 데이터를 찾을 수 없습니다." + dept.getDeptName());
    }
}
  • UPDATE 연산에 DeptDataNotFoundException에 메시지를 전달하여 출력할 수 있다.

▶️실습 - Dept Dao의 RowMapper 객체

RowMapper<Dept> rowMapper = new RowMapper<Dept>() {
    @Override
    public Dept mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Dept(
                rs.getLong("dept_no"),
                rs.getString("dept_name"),
                rs.getString("location")
        );
    }
};
  • Dept클래스의 필드 id에 데이터베이스의 컬럼 dept_no를 가져오고
    필드 deptName에는 데이터베이스의 컬럼 dept_name을 가져온다.
    필드 location은 데이터베이스의 컬럼 location을 가져오는 것이다.

  • 이들을 일치시켜주기위해서 RowMapper 객체를 직접 정의한 것

 

// dept 테이블 실습
//INSERT
deptDao.insertDept(new Dept(null, "FC Benfica", "Portugal"));
deptDao.insertDept(new Dept(null, "FC Dortmund", "Germany"));
deptDao.insertDept(new Dept(null, "Deportivo", "Spain"));

//UPDATE
deptDao.updateDept(new Dept(1L, "update1", "updateTest"));

//DELETE
deptDao.deleteDept(1L);

//SELECT
List<Dept> datas = deptDao.findAllDept();
for(Dept data : datas){
    System.out.println(data);
}

Dept테이블 Spring JDBC를 통한 CRUD연산 결과


🚀 RowMapper객체 사용과 CRUD연산을 Spring JDBC에서 활용하는 부분의 이해가 부족했었는데 

회고를 통해 공부할 수 있게 되었다. @RequiredArgsConstructor 를 사용하는 부분 또한 회고 시간에 추가 공부를 통해 더 이해를 잘할 수 있었다.