본문 바로가기
활동정리/모각코

2024 하계 모각코 1회차

by 잔디🌿 2024. 7. 13.

    목표 : 카카오테크캠퍼스에서 배운 JDBC 지식을 정리하고, 코드리뷰 내용을 정리한다.

     

    이제까지는 데이터베이스가 따로 없어서 해시맵에 저장해 두었었는데, 앱이 실용적으로 수행되려면 시스템을 껐다 켜도 데이터가 그대로여야한다. 

     

    데이터베이스는 데이터를 관리하기 위한 별도의 공간이고, 데이터베이스를 관리하고, 운영하는 소프트웨어를 DBMS라고 한다. DBMS에는 MySQL, 오라클 등이 있다.

     

    JDBC

     

    JDBC는 데이터베이스에 접속할 수 있도록 도와주는 자바 API이다.

    DBMS마다 접근 로직이 다르다. JDBC는 접근로직을 구현체로부터 분리하여 디비에 따라 코드 수정을 할 필요가 없도록 만들어준다.

     

    JDBC 구현하는 법

     

    1. 의존성 주입

    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    runtimeOnly 'com.h2database:h2'

    gradle의 의존성 부분에 위 코드를 추가한다.

     

    2.application.properties 설정 추가

    가끔 이 파일이 없는 경우가 있는데 src/main/resources에 추가하면 된다.

    # h2-console 활성화 여부
    spring.h2.console.enabled=true
    # db url
    spring.datasource.url=jdbc:h2:mem:test

    이렇게 하면 http://localhost:8080/h2-console 에서 데이터를 확인할 수 있다.

     

    JdbcTemplate

    JdbcTemplate는 스프링에서 SQL관계형 데이터베이스, JDBC와 쉽게 작업할 수 있도록 제공하는 객체이다.

    이를 사용하면 리소스 획득,연결 관리, 예외처리 등과 같은 작업은 고려하지 않고 쿼리와 응답처리만 고민할 수 있도록 해준다.

     

    테이블 생성

    package hello;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.jdbc.core.JdbcTemplate;
    
    @SpringBootApplication
    public class DemoApplication implements CommandLineRunner {
    
        public static void main(String args[]) {
            SpringApplication.run(DemoApplication.class, args);
        }
    
        @Autowired
        JdbcTemplate jdbcTemplate;
    
        @Override
        public void run(String... strings) throws Exception {
            
            jdbcTemplate.execute("DROP TABLE customers IF EXISTS");
            jdbcTemplate.execute("CREATE TABLE customers(id SERIAL, first_name VARCHAR(255), last_name VARCHAR(255))");
        }
    }

    테이블 생성 부분이다. 프로그램이 처음 실행되는 Appication 클래스 안에서 테이블을 만든다.

    우선 애플리케이션이 실행되면 바로 실행되는 부분을 구현하기 위해 CommandLineRunner을 인터페이스를 받는다.

    @Autowired를 사용하여 JdbcTemplate를 주입받는다. 애플리케이션이 실행될 때 스프링빈에 JdbcTemplate가 등록되기때문에 별도의 작업이 필요 없다.

    run메서드를 오버라이드하여  위에서 받은 객체를 사용해 테이블을 만든다. 

    jdbcTemplate.execute()메서드를 사용하여 sql문을 실행시킨다.

    만약 만들고자 하는 테이블이 이미 존재하면 삭제하고, 새로 테이블을 만든다.

     

    데이터 저장

      public void update(Long id, String name, int price, String imageUrl) {
            String sql = "UPDATE menus SET name = ?, price = ?,imageUrl = ? WHERE id = ?";
            jdbcTemplate.update(sql, name,price,imageUrl,id);
        }

    데이터를 저장하기 위해서는 우선 쿼리문을 작성한다.

    쿼리문에서 변수의 값은 ?로 남겨두면 된다. 그 다음 위와 같이 jdbcTemplate.update에, sql문과 ?에 들어갈 값을 차례로 넣으면 된다.

     

    데이터 조회

    public Menu findById(Long id) {
            String sql = "select id, name, price,imageUrl from menus where id = ?";
            return jdbcTemplate.queryForObject(
                    sql,
                    (resultSet, rowNum) -> new Menu(
                            resultSet.getLong("id"),
                            resultSet.getString("name"),
                            resultSet.getInt("price"),
                            resultSet.getString("imageUrl")
                    ),
                    id
            );
        }

    아까와 마찬가지로 해당 쿼리문을 만들고, jdbcTemplate.queryForObject에 넣어서 값을 조회한다.

    그 다음 위 쿼리문에서 나온 값이 resultSet에 저장되어있으니까 이를 꺼내서 새로운 객체를 만든다.

    rowNum은 현재 나오는 객체들의 값이 몇번째 데이터인지를 나타낸다. 여기서는 하나의 객체만 리턴하니까 사용하진 않는다.

     

    참고로 모든 데이터를 조회할 때에는

    public List<Menu> findAll(){
        String sql = "select id, name, price,imageUrl from menus";
        List<Menu> menus = jdbcTemplate.query(
                sql, (resultSet, rowNum) -> {
                    Menu menu = new Menu(
                            resultSet.getLong("id"),
                            resultSet.getString("name"),
                            resultSet.getInt("price"),
                            resultSet.getString("imageUrl")
                    );
                    return menu;
                });
        return menus;
    }

    위와 같이 jdbcTemplate.query()를 사용해서 구현한다.

     

    데이터 삭제

    public Long delete(Long id){
            var sql = "delete from menus where id = ?";
            jdbcTemplate.update(sql, id);
            return id;
        }

    delete쿼리문을 작성하고, 이를 jdbcTemplate.update를 사용하여 적용한다.

     

    데이터베이스 확인하기

     http://localhost:8080/h2-console에 접속한 후

     

     

    바로 connect를 누른다.

     

     

    그러면 이런 화면이 뜨고, 왼쪽에서 원하는 테이블명을 클릭하면 그에 맞는 SELECT문이 나온다. 여기서 RUN을 클릭하면 

     

     

     

    이렇게 데이터들이 출력되는 것을 볼 수 있다.

     

    https://ethereal-coder.tistory.com/215

     

    [카테캠] 데이터베이스 적용, JDBC

    이제까지는 데이터베이스가 따로 없어서 해시맵에 저장해 두었었는데, 앱이 실용적으로 수행되려면 시스템을 껐다 켜도 데이터가 그대로여야한다.  데이터베이스는 데이터를 관리하기 위한

    ethereal-coder.tistory.com

     

     1주차 과제 코드리뷰를 받았다. 사실 스프링으로 스스로 무언가를 구현하는 것이 처음이라 정말 어려웠고, 수업때 배운 내용을 최대한 활용해서 구현해보려고 노력했다. 그래도 과제 다 못할수도 있다고 생각했었는데 어찌저찌 하니까 되어서 다행이다.

     멋사 때 코드리뷰를 받아본 적이 있는데 그 때 실력이 많이 느는 것이 느껴졌었다. 카테캠에서는 깃허브 뿐만 아니라 실시간 줌으로 피드백 해주는 과정도 제공하기 때문에 앞으로 6주동안 얼마나 성장할 수 있을지 기대가 된다.

     

    내가 여쭤봤던 부분

    Q.

    코드를 짜다 보니 메서드가 뒤죽박죽인 것 같은데 어떤 순서로 정리하는 것이 좋을까요

    A.

    https://github.com/JunHoPark93/google-java-styleguide

     

    GitHub - JunHoPark93/google-java-styleguide: Google의 Java StyleGuide를 번역한 문서 📝

    Google의 Java StyleGuide를 번역한 문서 📝. Contribute to JunHoPark93/google-java-styleguide development by creating an account on GitHub.

    github.com

    여기서 말하는 순서대로 작성해주면 좋을 것 같습니다

     

    소스파일들은 

    1.라이센스나 저작권 정보

    2.패키지 구문

    3.import 구문

    4.정확히 하나의 최상위 클래스

     

    이 글을 읽어보니 오버로딩 부분을 붙여놓아야 한다는 것 빼고 정확한 기준은 없는 것 같았다. 대신 논리적인 순서를 따라야 한다고 했다.

    이번 과제에서는 CRUD를 주로 구현하니까 이 순서대로 정리해봐야겠다.

     

    Q.

    현재는 menu로 하나의 패키지를 구성했는데 나중에는 혹시 controller끼리, service끼리 묶어야하는지 궁금합니다

    A.

    도메인 단위로 패키지를 구성하는 방식과 controller, service등으로 묶는 계층형아키텍처 방법이 있는데, 어떤 것이 좋을지 생각해보세요

     

    내가 개인적으로 생각했을 때에는 도메인 이름은 이미 클래스 명에 들어가기 때문에 계층형 아키텍처방식으로 구현하는 것이 좋을 것 같다는 생각이 들었다.

    실제로 찾아보니까 도메인형과 계층형아키텍처형 둘 다 장단점이 있었다

     

    도메인형

    도메인을 기준으로 패키지를 나눔

    <장점>

    도메인별 응집도가 높아져 도메인의 흐름을 파악하기 쉽고, 도메인과 관련된 스펙, 기능이 변경되었을 때 변경 범위가 작다.

    <단점>

    애플리케이션의 전반적인 흐름을 파악하기 어렵고, 관점에 따라 어느 패키지에 위치할지 달라질 수 있다.

     

    계층형

    <장점>

    패키지 구조만 보고도 전체적인 구조를 파악할 수 있다.

    계층별 응집도가 높아져 계층별 수정이 있을 때 하나의 패키지만 보면 된다.

     

    <단점>

    도메인별 응집도가 낮아 도메인의 흐름을 파악하기 어렵다.

    규모가 커지면 하나의 패키지 안에 여러 클래스들이 모여서 구분이 어려워진다.

     

    나는 작은 규모의 프로젝트를 하고있기때문에 계층형으로 변경하였다.

     

    피드백 내용

    P1.

    생성자주입과 @Autowired 어노테이션을 이용한 주입에는 어떤 차이가 있을까요

     

    의존성 주입 방법에는 3가지가 있다.

     

    1. 생성자 주입

    생성자를 통해서 의존 관계를 주입하는 방식이다.

     

    생성자 주입은 생성자의 호출 시점에 1회 호출되는 것이 보장된다. 따라서 반드시 주입되어야 할 때 사용할 수 있다.

    클래스의 생성자가 하나라면 @Autowired를 생략할 수 있다.

    @Service
    pulbic class UserService {
    	// final 붙일 수 있음
    	private final UserRepository userRepository;
    
    	// @Autowired (생략 가능)
    	public UserService(UserRepository userRepository) {
    		this.userRepository= userRepository;
    		}
    }

     

    2. 필드주입

    필드에 의존관계를 주입하는 방법이다. 

    코드가 간결해지지만 외부에서 접근이 불가능하다. 테스트코드의 중요성이 부각되어 요즘에는 거의 사용하지 않는다.

    @Service
    public class UserService {
    	@Autowired
    	private UserRepository userRepository;
    }

     

    3. 수정자주입

    Setter을 사용하여 의존관계를 주입한다.

    생성자 주입과 다르게 주입받는 객체가 변겨될 가능성이 있는 경우에 사용한다.

    private class UserService {
    	private UserRepository userRepository;
    	@Autowired
    	public void setUserRepository(UserRepository userRepository) {
    		this.userRepository = userRepository;
    	}
    }

     

    생성자 주입방식이 가장 권장하는 주입방식이다.

     

    내 코드를 보면 다른 곳들은 다 생성자 주입방식을 사용하는데 main이 있는 Application.java에서는 필드 주입방식을 사용했었다.

    그래서 다시 생성자주입방식으로 변경했다!

    public Application(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

     

     

    P2.

    이 부분은 생성자체이닝을 활용해보세요

     

    생성자 체이닝이 뭔지 몰라서 알아봤다.

     

    생성자 체이닝이란

     

    여러 생성자 중에서 가장 많은 입력을 필요로 하는 생성자를 기준으로 입력할 값의 수가 적은 다른 생성자들을 이 기준이 되는 생성자를 통해 간접적으로 호출하는 방식이다.

    이렇게 하면 코드를 간결하게 만들 수 있다.

       public Person() {
            this("Empty");
        }
    
        public Person(String name) {
            this(name, 0);
        }
    
        public Person(String name, int age) {
            this(name, age, "Korea");
        }
    
        public Person(String name, int age, String country) {
            this.name = name;
            this.age = age;
            this.country = country;
        }

     

    이러한 코드가 있을 때 가장 아래에 있는 코드가 매개변수가 가장 많다. 

    이를 기준으로 모든 생성자가 만들어진다. 

    만약 매개변수가 2개인 생성자가 호출되면 해당 매개변수를 넣고, 나머지는 기본 값으로 채운 후, 다음 생성자를 호출한다.

    때문에 매개변수의 수와 상관없이 최종적으로는 기준이 되는 생성자가 마지막으로 호출된다.

     

      public Menu(String name, int price, String imageUrl) {
            this.name = name;
            this.price = price;
            this.imageUrl = imageUrl;
        }
        public Menu(long id,String name, int price, String imageUrl) {
            this.id = id;
            this.name = name;
            this.price = price;
            this.imageUrl = imageUrl;
        }

     

    public Menu(String name, int price, String imageUrl) {
            this(null, name, price, imageUrl);
        }
        public Menu(long id,String name, int price, String imageUrl) {
            this.id = id;
            this.name = name;
            this.price = price;
            this.imageUrl = imageUrl;
        }

     

    이를 내 코드에 적용시켰다.

     

     

    P3.

    불필요한 Setter 제거해주세요!

     

    이건 DTO만들 때 걍 다 만들어버려서 그런 것 같다. 앞으로는 Setter 필요할 때 그때그때 생성해야겠다.

     

    P4.

    @SpringBootApplication 어노테이션은 main에만 있어도 됩니다! @SpringBootApplication의 기능은 뭘까요

     

    어쩌다 들어갔는지는 잘 모르겠지만 Application클래스 말고도 다른 클래스에 스프링부트어플리케이션 어노테이션이 들어가있었다.

     

    @SpringBootApplication

    @SpringBootApplication은 auto-configuration을 담당한다.

    이 어노테이션으로 인해 스프링부트이 자동설정, 스프링 Bean 읽기와 생성이 자동으로 이루어진다.

     

    @SpringBootApplication이 있는 위치부터 설정을 읽어가기 때문에 이 어노테이션을 포함한 클래스는 항상 프로젝트의 최상단에 위치해야한다.

     

     

    P5.

    @RestController와 @Controller의 차이는 뭘까요

     

    Controller은 주로 View를 반환하기 위해서 사용된다.

    Spring에서는 Controller가 데이터를 반환해야 하는 경우가 생긴다.

    이 때 JSON형태로 반환하려면 @RequestBody 어노테이션이 필요하다

    @Controller
    @RequiredArgsConstructor
    public class UserController {
    
        private final UserService userService;
    
        @GetMapping(value = "/users")
        @ResponseBody
        public ResponseEntity<User> findUser(@RequestParam String userName){
            return ResponseEntity.ok(userService.findUser(userName));
        }
        
        @GetMapping(value = "/users/detailView")
        public String detailView(Model model, @RequestParam String userName){
            User user = userService.findUser(userName);
            model.addAttribute("user", user);
            return "/users/detailView";
        }
    }

    따라서 @Controller를 사용했을 때에는 데이터를 리턴하는 메서드에 @ResponseBody를 추가해야한다.

     

    @RestController은 @Controller + @ResponseBody이다.

    @RestController
    @RequiredArgsConstructor
    public class UserController {
    
        private final UserService userService;
    
        @GetMapping(value = "/users")
        public User findUser(@RequestParam String userName){
            return userService.findUser(userName);
        }
    
        @GetMapping(value = "/users")
        public ResponseEntity<User> findUserWithResponseEntity(@RequestParam String userName){
            return ResponseEntity.ok(userService.findUser(userName));
        }
    }

    @RestController를 사용했을 때에는 그냥 쓰면 된다.

     

    내 코드에서는 뷰 리졸버가 이를 처리해줘서 따로 @RestController를 사용하지 않아도 @ResponseBody를 쓸 필요가 없다.

    타임리프에서는 @RestController을 사용하면 작동을 하지 않아서 @Controller를 사용하였다.

     

    P6.

    @RequestBody에 대해 알아보는 것이 좋을 것 같아요

    @RequestBody는 클라이언트에서 서버로 Json 데이터를 요청 본문에 담아서 서버로 보내면, 서버에서는 @RequestBody 어노테이션을 사용하여 HTTP 요청 본문에 담긴 값들을 자바 객체로 변환시켜 객체에 저장한다.

    @PostMapping
    public String save(
            @ModelAttribute MenuRequest request
    ) {
        Menu newMenu = menuService.save(request.name(),request.price(),request.imageUrl());
        return "redirect:/menu";
    }

     

    내 코드에서는 @ModelAttribute가 HTML 폼 서브미션을 처리해줘서 따로 사용하지 않아도 된다.

     

    P7.

    simple jdbc insert에 대해 알아보고 이를 사용해보세요

     

    simple jdbc insert는 데이터베이스에 값을 직접 삽입하고 삽입 직후 데이터의 primaryKey를 얻기 위해 사용한다.

    public MenuRepository(JdbcTemplate jdbcTemplate, DataSource source) {
        this.jdbcTemplate = jdbcTemplate;
        this.jdbcInsert = new SimpleJdbcInsert(source)
                .withTableName("MENU")
                .usingGeneratedKeyColumns("id");
    }

    우선 생성자 부분에서 jdbcInsert를 만든다. insert할 테이블 명을 적고, 반환할 키값을 쓴다. menu의 PK는 id라 id를 적었다.

    public Menu save(Menu menu) {
        try {
            SqlParameterSource params = new MapSqlParameterSource()
                    .addValue("name", menu.getName())
                    .addValue("price", menu.getPrice())
                    .addValue("imageUrl", menu.getImageUrl());
    
            Long id = jdbcInsert.executeAndReturnKey(params).longValue();
            return new Menu(id, menu.getName(), menu.getPrice(), menu.getImageUrl());
        } catch (DuplicateKeyException e) {
            System.err.println("Duplicate key error: " + e.getMessage());
            return null;
        }
    }
    

     그 다음 위와 같이 구현하였는데 에러가 난다.. 이 부분은 다시 여쭤봐야겠다.

     

     

     

    P8.

    row mapper를 멤버변수로 분리하면 중복코드를 제거할 수 있을 것 같아요

     

    rowMapper은 row단위로 ResultSet의 row를 매핑하기 위해 JdbcTemplate에서 사용하는 인터페이스이다.

    ResultSet의 데이터의 각 행을 객체 형태로 변환해주고, count수만큼 반복한다.

    .query의 쿼리문에서 나온 행들을 mapRow가 받아서 객체로 변환시켜준다고 생각하면 편한 것 같다.

     

    private final RowMapper<Menu> menuRowMapper = new RowMapper<Menu>() {
        @Override
        public Menu mapRow(ResultSet rs, int rowNum) throws SQLException {
            return new Menu(rs.getLong("id"), rs.getString("name"), rs.getInt("price"), rs.getString("imageUrl"));
        }
    };
    public Menu findById(Long id) {
        String sql = "select id, name, price,imageUrl from menus where id = ?";
        return jdbcTemplate.queryForObject(
                sql,
                menuRowMapper,
                id
        );
    }
    
    public List<Menu> findAll() {
        String sql = "select id, name, price,imageUrl from menus";
        List<Menu> menus = jdbcTemplate.query(
                sql,
                menuRowMapper);
        return menus;
    }

    이런 식으로 중복코드를 제거하였다.

    query 부분이 sql을 실행하여 resultSet를 받고 이를 menuRowMapper에 전달하여 원하는 객체를 받는다. 

     

    그냥 교재에 나와있어서 의미를 정확하게 모르고 사용한 부분과 몰라서 깔끔하지 못한 코드를 짠 부분을 수정할 수 있어서 좋았다!

    앞으로의 과제에서는 위에서 배웠던 내용을 더 적용해보아야겠다.

     

    참고

    https://velog.io/@sujin1018/%EC%8A%A4%ED%94%84%EB%A7%81-Spring-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85-%EB%B0%A9%EB%B2%95-%EB%B0%8F-%EC%83%9D%EC%84%B1%EC%9E%90-%EC%A3%BC%EC%9E%85-%EB%B0%A9%EB%B2%95%EC%9D%98-%EC%9E%A5%EC%A0%90

     

    [스프링] Spring 의존성 주입 방법 및 생성자 주입 방법의 장점

    @Autowired와 private final 의 차이점에 대해 알아보다가 의존성 주입 방법과 생성자 주입 방법의 장점에 대해 정리하였다. 생성자를 통해 의존 관계를 주입하는 방법 생성자 주입은 생성자의 호출 시

    velog.io

    https://developer-talk.tistory.com/227

     

    [Java]생성자 체인(Constructor Chaining)

    이번 포스팅에서는 Java에서 객체를 생성할 때, 생성자에서 다른 생성자를 호출하는 생성자 체인(Constructor Chaining)에 대해 설명합니다. 생성자 체인(Constructor Chaining) 생성자 체인은 this 또는 super

    developer-talk.tistory.com

    https://coooding.tistory.com/33

     

    [Spring][Spring boot] @SpringBootApplication 이란?

    다들 스프링부트 프로젝트를 처음 시작할때 @SpringBootApplication를 한번씩 보셨을 것입니다. 오늘은 @SpringBootApplication에 대해서 간단히 알아보겠습니다. @SpringBootApplication 이란? @SpringBootAplication 어

    coooding.tistory.com

    https://mangkyu.tistory.com/49

     

    [Spring] @Controller와 @RestController 차이

    Spring에서 컨트롤러를 지정해주기 위한 어노테이션은 @Controller와 @RestController가 있습니다. 전통적인 Spring MVC의 컨트롤러인 @Controller와 Restuful 웹서비스의 컨트롤러인 @RestController의 주요한 차이점

    mangkyu.tistory.com

    https://hyeon9mak.github.io/easy-insert-with-simplejdbcinsert/

     

    SimpleJdbcInsert를 통한 쉬운 Insert

    ```java private final JdbcTemplate jdbcTemplate;

    hyeon9mak.github.io

     

    소감

     

    이제까지 스프링을 개발동아리에서 배웠었는데, 그 때 배운 내용을 더욱 잘 이해하게 되는 것 같아서 좋았고, 코드리뷰 받아보니까 단순히 구현이 중요한게 아니라 어떤 코드가 유지보수성에 좋고, 보안에 좋은지를 고민하는 것이 훨씬 중요하다는 것을 알게 되었다.

    앞으로 더욱 열심히 공부해서 좋은 코드를 짜기 위해 노력해야겠다.