본문 바로가기
프로젝트/selfmade Blog - V1 (deprecated)

3. 블로그 글 작성 기능 - 글 생성 및 중복 Context 추출

by zangsu_ 2023. 8. 11.

가장 먼저, 블로그 글 CRUD 로직을 구현해 보자.

 

모델 생성

클래스 생성

블로그 글에는 제목과 글 내용, 작성자가 필요하다. 추가로, 블로그 포스팅을 구분하기 위한 인덱스 값까지 네개의 필드로 `Posting` 객체를 만들어 주자.

@Data  
public class Posting {  
    int idx;  
    String title;  
    int user_idx;  
    String content; 
    
public Posting(String title, int user_idx, String content) {  
        this.title = title;  
        this.user_idx = user_idx;  
        this.content = content;  
    }  
}

 

테이블 생성

DB에도 게시글을 저장하기 위한 테이블을 생성해 둔다.

create table posting(
idx int auto_increment primary key,
    title char(20) not null, 
    user_idx int not null,
    content char(255) not null,
    foreign key (user_idx) references users(idx)
);

 

DAO 생성

그리고, `Posting`의 DB관련 로직을 구현할 `PostingDAO`를 생성해 준다.

public class PostingDAO {  
  
    private final String url = DAOConfig.URL;  
    private final String userName = DAOConfig.USERNAME;  
    private final String password = DAOConfig.PASSWORD; 
    private static PostingDAO postingDAO = null;   
  
    public static PostingDAO getPostingDAO(){  
        if(postingDAO == null)  
            postingDAO = new PostingDAO();  
        return postingDAO;  
    }  

//...
}

 

DB에 접근하기 위한 url, userName, password는 한 곳에서 관리를 하는 것이 좋을 것 같아 `DAOConfig` 클래스를 생성 후 내부에 final static String`으로 값을 저장해 두었다. 

그리고, PostingDAO` 객체를 싱글톤으로 관리하기 위한 코드를 함께 작성해 주었다.

 

로직 구현

저장 기능

이제, `PostingDAO`에서 `Posting`을 저장하기 위한 로직을 구현해 본다.

public void save(Posting posting, User user){  
    try(Connection conn = DriverManager
    .getConnection(url, userName, password)) {  
  
        PreparedStatement pstm = conn.prepareStatement(
        "insert into posting (title, user_idx, content) VALUES (?,?,?)");  
        pstm.setString(1, posting.getTitle());  
        pstm.setString(2, Integer.toString(user.getIdx()));  
        pstm.setString(3, posting.getContent());  
  
        pstm.executeUpdate();  
    } catch (SQLException e) {  
        e.printStackTrace();  
    }  
}


`save()` 함수의 동작을 확인할 테스트 코드를 작성하자.

class PostingDAOTest {
	User user = new User("new user", "user password");  
    Posting posting = new Posting("title1", user.getIdx(), "content1");  

    @Test  
    @Transactional  
    public void save() throws Exception{  
        //given  
        userDAO.save(user);  

        //when  
        postingDAO.save(posting, user);  

        //then  
    }
}


그런데 오류가 발생한다...

java.sql.SQLIntegrityConstraintViolationException: 
Cannot add or update a child row: 
a foreign key constraint fails 
(`selfmadeblog`.`posting`,
CONSTRAINT `posting_ibfk_1` FOREIGN KEY (`user_idx`)
REFERENCES `users` (`idx`))
at co m.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:118) 
at co m.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) 
//...//
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)


외래키 참조에 문제가 생겼다는데, 쿼리가 어떻게 날아갔는지를 확인해 봐야 할 것 같다.

생성된 쿼리를 출력해 확인해 보자.

다음이 `PreparedStatement` 쿼리문이다. 

insert into posting (title, user_idx, content) VALUES ('title1','0','content1')

`User`의 인덱스로 0이 사용되고 있다. 

`UserDAO`의 저장 로직을 다시 확인해 보자.

//save() 함수의 일부
PreparedStatement pstmt = connection.prepareStatement(
"insert into users (id, password) VALUES (?, ?)");   
pstmt.setString(1, user.getId());  
pstmt.setString(2, user.getPassword());  

pstmt.executeUpdate();  
connection.close();  

//User 클래스
@Data  
public class User {  
  
    int idx;  
    String id;  
    String password;  
  
    public User(String id, String password) {  
        this.id = id;  
        this.password = password;  
    }  
}


`User`의 `idx` 필드는`auto_increment`의 사용으로 생성시에 초기화 해 줄 수 없기 때문에 저장 시점에 변경을 해 주어야 한다. 그러나, `User`의 저장 로직은 저장 이후 별다른 동작을 하고 있지 않기 때문에 문제가 생긴 것이다.
`User`의 저장 이후 `idx` 값을 업데이트 해 주자.

//변경 이후의 save() 코드 일부
PreparedStatement pstmt = connection.prepareStatement(
	"insert into users (id, password) VALUES (?, ?)",
	Statement.RETURN_GENERATED_KEYS);  
pstmt.setString(1, user.getId());  
pstmt.setString(2, user.getPassword());  
  
pstmt.executeUpdate();  

ResultSet rs = pstmt.getGeneratedKeys();  
if(rs.next()){  
    user.setIdx(rs.getInt(1));  
}  
  
connection.close();

 

`pstmt.getGeneratedKeys()` 메서드를 사용해 자동으로 생성된 키의 값을 받아 올 수 있으며, 이를 위해 `PreparedStatement`를 생성할 떄 `Statement.RETURN_GENERATED_KEYS`을 함께 사용해 주어야 한다.

 

테스트 성공이다!


중복 로직 추출

앞으로도, SQL을 실행하고 생성된 키를 받아오는 로직은 많이 사용될 것 같으니 해당 문맥 자체를 추출해서 재사용 하는 편이 좋아 보인다.

우선 DAOContext의 메서드를 정의할 `DAOInterface`를 만들자.

public interface DAOInterface {  
    public int executeSQLAndReturn(String sql, String... args);  
}

우선 간단하게, SQL을 실행하고, `auto_increment` 등으로 자동으로 생생되는 값을 리턴해 주는 메서드 하나만 정의해 두자.
해당 메서드에는 `String` 타입의 SQL과 해당 SQL에 동적으로 들어갈 변수들만 전달을 해 줄것이다. 이 떄 전달될 변수의 개수는 정해지지 않았으므로 `String...` 으로 정의해 두었다.

다음으로는 실제로 문맥을 작성할 `DAOContext` 클래스를 작성하자.

public class DAOContext implements DAOInterface{  

private static DAOContext daoContext = null;
    private static final String url = DAOConfig.URL;  
    private static final String userName = DAOConfig.USERNAME; 
    private static final String password = DAOConfig.PASSWORD; 

//싱글톤을 유지하기 위한 private 생성자, getDAOContext(), 
  
    @Override  
    public int executeSQLAndReturn(String sql, String... args) {  
        int generatedKey = 0;  
        
        try (Connection conn = getConnect()) {  
        
            PreparedStatement ps = conn.prepareStatement(
            sql, Statement.RETURN_GENERATED_KEYS);  
            for(int i = 0; i<args.length; i++)
                ps.setString(i+1, args[i]);  
            ps.executeUpdate();  
  
            ResultSet rs = ps.getGeneratedKeys();  
            if (rs.next())   
                generatedKey =  rs.getInt(1);  
             
        } catch (SQLException e) {  
            e.printStackTrace();  
        }  
  
        return generatedKey;  
    }  
  
    public Connection getConnect() throws SQLException {  
        return DriverManager.getConnection(
        url, userName, password);  
    }  
}

위와 같이 전체 문맥을 추출해 `executeSQLAndReturn()` 메서드를 만들고, 생성된 값을 리턴해 주었다.

이제 새로 만든 메서드를 적용해 보자.

public void save(Posting posting, User user){  
    String sql = "insert into posting (title, user_idx, content) VALUES (?,?,?)";  
    int generatedKey = daoContext.executeSQLAndReturn(sql,
       posting.getTitle(),
       Integer.toString(user.getIdx()), 
       posting.getContent());  
    posting.setIdx(generatedKey);  
}

 

성공이다!!!

물론 지금은 각각의 DAO 클래스들이 DAOContext라는 구현 클래스에 의존하고 있다는 점이 아쉽지만, 이번 프로젝트에서는 의존성 주입보다는 전체 기능 구현에 좀 더 집중 하려고 하기에 넘어 가도록 하자.

댓글