SpringFramework/BASIC

[Jdbc-Template] select문 / query() , queryForObject() / RowMapper<Type> / 익명클래스, 람다식 , 내부클래스 활용

유혁스쿨 2020. 9. 2. 15:57
728x90
반응형

Jdbc-Template를 활용한 select처리

query() Multi row
쿼리문 수행결과가 한개 이상 List로 반환해줍니다
queryForObject() Single row 쿼리문 수행결과가 한개 객체 그대로 반환해줍니다.

 

Jdbc-Template의 select는 query() 와 queryForObject() 라는 메서드를 사용합니다.

 

기존 JDBC의 executeQuery()나 executeUpdate()메소드와 이름에서 공통점을 찾을수 있습니다.

 

query(sql, new Object[] {}, new RowMapper<Type>() {

	익명클래스
    
});

 

query()메서드의 매개값으로는 'sql' , 'new Object[] {}' , 'newRowMapper<Type>()'을 인자값으로 받으며

제네릭타입에 Type과 맞는 익명클래스 로직방식으로 작성해줍니다.

 

- 첫번째 인자값에는 수행할 sql쿼리문을 넣어줍니다.

- 두번째 인자값으로는 sql물음표값 즉, 바인딩변수에 세팅될 값을 넣어줍니다.

   Object[] 배열 객체를 넣는이유는 바인딩변수를 여러개 지정해야할 때도 있기 때문입니다. 

- 세번째 인지값으로는 RowMapper<Type> 객체를 넣어주고 있습니다.

기존의 ResultSet을 사용할 때 DB컬럼에 있는 데이터를 받아 setter()를 통해 VO에 저장해주는데 그 과정을 담아 대신 처리해주는 역할을 해주는RowMapper객체를 넣어달라는것 입니다.

 

또한 RowMapper<Type>의 제네릭타입인 Type과 맞는 익명클래스를 구현해줘야 합니다. 

 

우선 간단한 예제를 통해 좀더 설명해드리겠습니다.

@Repository 
public class UserDAO implements IUserDAO {
    
    private String driver = "oracle.jdbc.OracleDriver";
    private String url = "jdbc:oracle:thin:@127.0.0.1:1521:xe";
    private String user = "week";
    private String pw = "week";

    @Override
    public List<ScoreVO> selectAllUsers() {
        List<UserVO> list = new ArrayList<UserVO>();
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        String sql = "select * from users";
        try {
            con = DriverManager.getConnection(url,user,pw);
            pstmt = con.prepareStatement(sql);
            rs = pstmt.executeQuery();
            while(rs.next()) {
                UserVO users = new UserVO();
                scores.setStuName(rs.getString("user_name"));
                list.add(users);
            }
        } catch (Exception e) {
			e.printStackTrace();
        }finally {
			try {
                if(rs!=null)rs.close(); if(pstmt!=null)pstmt.close(); if(con!=null)con.close();
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return list;
    }
}

 

 

RowMapper는 전통방식의 JDBC코드에서 while문의 범위를 말합니다.

while(rs.next()) {
	UserVO users = new UserVO();
	scores.setStuName(rs.getString("user_name"));
	list.add(users);
	}

while문이 처리해주는 내용을 설명하자면 ResultSet의 결과를 받아 온 다음부터 rs.next()로 행들을 조회한 후 VO에 setter()를 매칭시켜서 받아옵니다

받아온 객체를 List에 한번 더 넣어주게됩니다.

 

반복문으로 반복하여 여러 정보들이 들어있는 객체들을 List에 담아주게되는것이죠.

이런 처리를 대신해주는것이 RowMapper<Type>라고 보시면 됩니다.

 

RowMapper의 제네릭에 해당하는 Type을 만들기 위해서는 클래스가 하나 있어야합니다.

public class UserMapper implements RowMapper<UserVO>{

}

이 클래스의 목적은 Jdbc Template에서 ResultSet사용을 편하게 하기위한 클래스 입니다.

그렇기 때문에 ResultSet의 역할을 해주는 RowMapper인터페이스를 구현해줘야 합니다.

이때 RowMapper<T> 으로 구현이되는데 이 제네릭타입에는 어떤VO에 ResultSet을 담을것인가 를 뜻하므로

담을 VO클래스를 타입으로 선언합니다.

 

메서드를 오버라이딩 합니다.

mapRow라고 불리는 메서드를 오버라이딩 해달라는것 입니다.

 

public class UserMapper implements RowMapper<UserVO>{
	@Override
	public UserVO mapRow(ResultSet rs, int rowNum) throws SQLException {
		return null;
	}
}

 

mapRow()메서드의 매개변수로는 ResultSet rs, int rowNum을 받게되는데

ResultSet에는 select 결과 테이블이 담겨있습니다.

rowNum은 행의 수입니다. 만약 데이터가 50개 즉, 50행이 나왔으면 50. 100행이 나왔으면 100이 됩니다.

데이터 결과를 담고있는 ResultSet과 데이터결과로 나온 행의수를 인자값으로 넣어줌으로써 어떻게 처리해야하는지를 얘기 해줍니다. 

어떤setter에 어떤cullum을 넣을것인가를 정의합니다.

public class UserMapper implements RowMapper<UserVO>{
	@Override
	public UserVO mapRow(ResultSet rs, int rowNum) throws SQLException {
    	UserVO user = new UserVO();
        user.setUserName(rs.getString("user_name")
		return users;
	}
}

 while문으로 반복했던 작업의 내용과 같죠?

 

다시한번 정리하겠습니다.

 

RowMapper인터페이스가 제공해주는 추상메서드 mapRow()메서드를 를 오버라이딩 해 줍니다.

이때 구현받는 RowMapper<T>의 제네릭타입이 UserVO이면 자동으로 메서드의 반환타입이 UserVO가 되도록 메서드가 추상화되있습니다.

그리고 메서드 내부에는 해당 UserVO를 어떻게 세팅할것인지에 대한 내용의 로직을 작성해줍니다.

"VO를 어떤 setter()메서드에 어떤 컬럼을 매칭시킬것인가"

 

그리고 이것을 DAO에서 받아줍니다.

@Repository 
public class UserDAO implements IUserDAO {

    @Autowired
    private JdbcTemplate template;

    @Override
    public List<UserVO> selectAllUsers() {
        String sql = "select * from users";
        return template.query(sql, new UserMapper());
    }
}

 

 

만약 return에서 가독성때문에 이해가 잘 안된다면 따로 new UserMapper(); 객체를 생성하고 레퍼런스 변수에 담아보겠습니다

    @Override
    public List<UserVO> selectAllUsers() {
        String sql = "select * from users";
        UserMapper um = new UserMapper();
        return template.query(sql, um);

또는

 

부모인터페이스 타입으로도 선언이 가능합니다

    @Override
    public List<UserVO> selectAllUsers() {
        String sql = "select * from users";
        RowMapper<UserVO> um = new UserMapper();
        return template.query(sql, um);

 예 : List<> list= new ArrayList<>();

 

하지만 레퍼런스 변수 um은 return에서 딱 한번만 사용되기때문에 제일 처음코드했던 방식으로 줄여주는것도 좋습니다.

코드하는 사람마다 방식 다르니 회사에서 사수가 지적하면 따라주시면 됩니다.

 

 

다시 정리해보겠습니다. 

query()메서드에 sql문과 함께 UserMapper의 객체 um을 넣어주면 들어온 쿼리문이 수행됨과 동시에 UserMapper클래스에 오버라이딩된 mapRow()메서드를 호출하게되는것입니다.

 

 이때 우리가 mapRow()메서드에 구현해놓은 로직을 구동시켜 mapRow메서드의 매개값인

'ResultSet에는 query()에 넣은 sql을 수행한 결과값이 오게되고,

int rowNum이 ResultSet에 들어온 수행된 sql쿼리문의 결과값 목록 행을 알아서 세어주는것"

 

이며 우리가 구현해놓은

'rs.get()메서드로 컬럼의 값을 받아와 UserVO객체에 setter()메서드를통해 값을담는로직'

이 적용되어서 다시 원래 메서드 위치로 결과를 반환 해 주는것이죠(void).

 

 

한가지 번거롭다고 느껴진다는것이 있는데

RowMapper인터페이스를 구현받은 클래스를 따로 직접 구현 해야 한다는 것입니다.

query()메서드에서 딱 한번만 쓸껀데 굳이 클래스를 따로 만들어야해?

 

그럴때 사용되는것이 익명클래스 입니다.

 

우선은 query의 두번째 인자값 new UserMapper();를 다음과 같이 변경합니다.

query(sql, new UserMapper());
query(sql, new RowMapper<UserVO>());

new RowMapper() 인데 UserVO타입으로 제네릭이 설정했습니다.

하지만 이것은 틀린문법입니다.

RowMapper는 인터페이스며 인터페이스는 객체생성이 안됩니다.

 

일단 query()메서드 에서는 new UserMapper를 사용하기 위해서 클래스를 만들어야합니다.

어? 난 한번만사용만 사용할건데 클래스를 새로 만드는게 과연 합리적인가? 

그럼 클래스를 new RowMapper<UserVO>(){ 여기서 만들면 안되나? } 파일로 따로 빼지않고?

이것은 익명클래스 방식이며 변경한코드처럼 인터페이스를 new연산자로 선언하면서 { }블록을 추가로 선언합니다.

query(sql, new RowMapper<UserVO>(){"클래스 영역"});

{ }블록안에 마치 클래스가 생성된것마냥 클래스에서 써야할 로직을 넣어주는것입니다.

 

 

UserMapper클래스에 선언된 로직을 말하는것입니다.

@Override
public ScoreVO mapRow(ResultSet rs, int rowNum) throws SQLException {
    String sql = "select * from users";
    UserMapper um = new UserMapper();
    return template.query(sql, um);
});

↑바로 이코드가 UserMapper클래스에 썼던 로직이 되겠죠

 

 

    @Override
    public List<UserVO> selectAllUsers() {
        String sql = "select * from users";
        return template.query(sql, new RowMapper<ScoreVO>() {//익명클래스 구현
        @Override
        public ScoreVO mapRow(ResultSet rs, int rowNum) throws SQLException {
            String sql = "select * from users";
            UserMapper um = new UserMapper();
            return template.query(sql, um);

        });
    }

이것이 바로 익명클래스 문법입니다.

 

람다식도 활용할 수 있습니다 ㅋㅋㅋㅋㅋㅋㅋ

 

[ 익명클래스 ↓ ]

        return template.query(sql, new RowMapper<ScoreVO>() {//익명클래스 구현
        @Override
        public ScoreVO mapRow(ResultSet rs, int rowNum) throws SQLException {
        				.
          				.
        				.
        });
    }

 

[ 람다식  ↓ ]

        return template.query(sql, (rs,rowNum) -> {//람다식 구현
        				.
          				.
        				.
        }

이렇게 표현해버릴수 있습니다...

 

    @Override
    public List<UserVO> selectAllUsers() {
        String sql = "select * from users";
       return template.query(sql, (rs,rowNum) -> {
            String sql = "select * from users";
            UserMapper um = new UserMapper();
            return template.query(sql, um);

        });
    }

 

조금은 어려운 자바 기초문법 입니다..

 

 

제가 부랴부랴 정리해놓은 익명클래스와 람다식의 개념입니다.

익명클래스와 람다식 링크 : https://u-it.tistory.com/33

 

 

[익명클래스]

인터페이스의 추상화된 기능을 특정 클래스를 생성해서 클래스에 정의된 내용을 딱 한번만 사용하고 싶을때 합리적으로 사용할수 있는 방법입니다.

 

[람다식]

인터페이스를 통해서 익명 클래스를 만들어 사용하려는데 어? 가만보니 인터페이스에 선언된 추상메서드가 한개만있네? 하나만 있는데 그냥 값만받아주자!

하고 값만 받고 -> 화살표와 { } 문법을 사용해서 받은 매개값을 -> 전달하여 { } 블록안에 추상메서드 add()에 직접 구현할 로직을 작성하면 되는것입니다.

 

RowMapper 인터페이스에 선언된 단 1개의 추상메서드 mapRow() 

 RowMapper에는 단1개의 추상메서드인 mapRow()메서드만 존재하므로 람다식까지 구현이 가능했던 것이죠.

 

 

다시 Select처리로 돌아가보겠습니다.

그렇다면 만약 DB로부터 select 한 모든 데이터를 담은 List가아닌 단 하나의 데이터만 가져온다고 할때, 어떻게 처리할까요?

여러 정보들을 담지 않을테니 List에 담을필요가 없겠고 그렇다면 List를 사용하지도, 반환하지도 않으면 될것입니다.

 

    @Override
    public UserVO searchUser(int userNum) {
        UserVO users = null;
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        String sql = "select * from users where user_id=?";
        try {
            con=DriverManager.getConnection(url,user,pw);
            pstmt = con.prepareStatement(sql);
            pstmt.setInt(1, userNum);
            rs = pstmt.executeQuery();
            if(rs.next()) {
                users = new UserVO();
                users.setUserName(rs.getString("user_name"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
                try {
                    if(rs != null) rs.close();
                    if(pstmt != null) pstmt.close();
                    if(con != null) con.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
        }
		
        return users;
    }
    		String sql = "select * from scores where stu_id=?";

쿼리문에 바인딩변수 ?가있네요

 

query까지만 치고 Ctrl+Space키를 눌러보면 메서드의 매개값 형태 목록 메뉴얼이 나옵니다

바인딩 변수를 Oobject배열로 받을때

Object[] args로 두번째인자값에 바인드변수를 Object[] 배열 형태로 받아 넣을수 있도록 설정되어 있습니다.

    @Override
    public UserVO searchUser(int userNum) {

        String sql = "select * from users where user_id=?";
        UserVO users = template.query(sql,new Object[] {stuNum}, new ScoreMapper());
		
        return users;
    }

 

바인딩 변수를 배열로 받지 않고 콤마, 를 이용하여 순서대로 주입하고싶을땐 어떻게 해야할까요?

바인딩 변수를 , , , 순서대로 기입하여 받을 때

query()메서드 중에 매개값이 Object... args를 받도록 해주는 매개변수 옵션이 있네요.

다만 순서가 마지막으로 바뀝니다.

    @Override
    public UserVO searchUser(int userNum) {

        String sql = "select * from users where user_id=?";
        UserVO users = template.query(sql,new ScoreMapper(), stuNum);
		
        return users;
    }

 

어? 그런데 다시보니 query()메서드의 리턴 타입이 List라고 되어있습니다..

반환타입이 List<T>

형광펜 바로뒤에 List<T>가 보이시죠...?

 

하지만 searchUser()메서드는 UserVO를 반환 해 주고 있습니다.

그러므로 query( ) 메서드를 사용할 수 없으며 queryForObject( ) 메서드를 사용해야합니다.

 

queryForObject()는 Single row를 리턴할 때 사용합니다.
query( )는 Multi row를 리턴할때 사용합니다.


싱글로우 : 조회결과가 단 1줄 -> VO
멀티로우 : 조회결과가 여러 줄 -> List

 

String sql = "select * from users where user_id=?";

수행결과가 딱 한줄만 나오는 쿼리문입니다. 

싱글로우 입니다.

 

그러므로 queryForObject( )메서드를 사용합니다. ↓ 

    @Override
    public UserVO searchUser(int userNum) {

        String sql = "select * from users where user_id=?";
        UserVO users = template.queryForObject(sql,new ScoreMapper(), stuNum);
		
        return users;
    }

 

 ↓익명클래스 문법을 적용합니다.

    @Override
    public UserVO searchUser(int userNum) {

        String sql = "select * from users where user_id=?";
        return template.queryForObject(sql, new RowMapper<UserVO>() {
            @Override
            public UserVO mapRow(ResultSet rs, int rowNum) throws SQLException {
                UserVO users = new UserVO();
                users.setUserName(rs.getString("user_name"));

                return users;
            }
        }, userNum);
    }

  람다식으로 변경

    @Override
    public UserVO searchUser(int userNum) {

        String sql = "select * from users where user_id=?";
        return template.queryForObject(sql, (rs,rowNum) -> {
            
                UserVO users = new UserVO();
                users.setUserName(rs.getString("user_name"));

                return users;
        }, userNum);
    }

 

하지만 이런 경우에는 가독성이 많이 떨어집니다.

RowMapper의 순서가 중간으로 가있기 때문에 양쪽 인자값의 식별성이 떨어집니다.

이런경우는 익명클래스가 좋은방법이라고 볼 수 없습니다.

 

중첩클래스인 내부클래스를 사용하는 방법이 있습니다.

 

public을 제거하여 패키지프랜들리로 다른 패키지에서 못쓰게 막도록 해줍니다

@Repository 
public class UserDAO implements IUserDAO {
    class UserMapper implements RowMapper<UserVO>{

    @Override
    public UserVO mapRow(ResultSet rs, int rowNum) throws SQLException {
        UserVO users = new UserVO();
        users.setUserName(rs.getString("user_name"));
        return users;
    }

}

중첩클래스는 클래스내부에 클래스를 하나 더 생성해줍니다.

컴파일과정에서 클래스 내부에 클래스가 하나 더생깁니다.

이렇게 하면 우리가 클래스파일을 따로 생성하지 않아도 new ScoreMapper()를 매개값으로 사용할 수 있습니다.

매번 select할때 같은내용의 익명 클래스 코드를 하지 않아도 되는것 입니다.

(Select List 혹은 Select One 따로 해야하는 경우 혹은 바인딩 조건식이 다른경우 여러번 사용될 수 있습니다.)  

 

그럼 UserMapper 클래스 파일이 없어도 됩니다. 삭제하시면 됩니다.

 

@Repository 
public class UserDAO implements IUserDAO {
    class UserMapper implements RowMapper<UserVO>{

    @Override
    public UserVO mapRow(ResultSet rs, int rowNum) throws SQLException {
        UserVO users = new UserVO();
        users.setUserName(rs.getString("user_name"));
        return users;
    }
    
    @Override
    public UserVO searchUser(int userNum) {

        String sql = "select * from users where user_id=?";
        UserVO users = template.queryForObject(sql,new ScoreMapper(), stuNum);
		
        return users;
    }
}

 

Jdbc-Template 대신 대중적으로 많이 사용하는 mybatis의 SqlSessionTemplate는

ResultSet을 하여 데이터를불러와 객체에 하나씩 담아주는 처리까지도 자동으로 해줍니다.....

프레임워크 끝장나네요....

 

이번 포스팅에서는 JdbcTemplate를 활용한 select구문 처리와 그에따른 문법 익명클래스,람다식, 내부클래스를 포스팅 내용으로 다뤘습니다.

 

감사합니다.


+번외 오류 

 

 

org.springframework.beans.factory.NoSuchBeanDefinitionException:

No qualifying bean of type 'com.spring.database.jdbc.score.repository.IScoreDAO' available:

expected at least 1 bean which qualifies as autowire candidate.

Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

라는 오류가 뜨실수 있습니다.

 

오류내용을 해석하면

 

사용 가능한 'com.spring.database.jdbc.score.repository.IScoreDAO'유형의 자격을 갖춘 Bean이 없습니다.

autowire 후보로 적격 한 최소 1 개의 bean이 필요합니다.

종속성 주석 : {@ org.springframework.beans.factory.annotation.Autowired (required = true)}

 

 

@Autowired옵션은 (required = true)인 true가 기본값이며 @Autowired(required=false)로 처리해주면 해당 타입의 빈 객체가 존재하지 않는 경우 예외 처리를 해줍니다

 

@Repository 어노테이션은 @Autowired가 선언된 컨트롤러에 해줘야 합니다.

컨테이너에 빈 등록이되어서 @Autowired로 template객체를 의존성 주입 했을때  <annotation-driven/>와 componant-scan이 동작하여 temlpate를 필요로 하는 빈등록된 컨트롤러를 열심히 찾습니다.

하지만 template객체가 필요한 컨트롤러에 @Repository선언을 안했다거나 다른 외부 클래스에 지정되거나 혹은 인식이 안되는경우에 

Autowired가... 이별택시노래를 부르게됩니다. 어디로 가야 하죠 아저씨... 나를 필요한 컨트롤러 빈이 없어요..하면서 오류를 띄우는거죠

 

 만약 내부클래스를 활용할 때, 클래스내부에 선언해야 될것을 실수로 클래스 외부에 선언하시어 Component기능을 해주는 어노테이션들이 내부 클래스위로 밀려서 선언될 가능성이 큽니다

혹은 컴포넌트 어노테이션을 하지 않으므로써 @Autowired로 선언된 의존객체가 <annotation-driven/>에 의해서 누군가가 나를 필요로 하는구나! 그래서 <component-scan/>을 했는데 나를 필요로 했던 클래스의 빈이 없다.....

오류내용이 완전 이별택시 가사입니다.. 

 

이 포스팅 코드를 하다가 뜬 오류라면 실수로 내부클래스를 클래스 밖에 선언하셨을 가능성이 큽니다.

@Repository 
public class UserDAO implements IUserDAO {
    class UserMapper implements RowMapper<UserVO>{

    @Override
    public UserVO mapRow(ResultSet rs, int rowNum) throws SQLException {
        UserVO users = new UserVO();
        users.setUserName(rs.getString("user_name"));
        return users;
    }
    
    @Autowired
    private JdbcTemplate templatei;

}

정상코드

@Repository //위로 밀려난 @Repository
class UserMapper implements RowMapper<UserVO>{

@Override
public UserVO mapRow(ResultSet rs, int rowNum) throws SQLException {
    UserVO users = new UserVO();
    users.setUserName(rs.getString("user_name"));
    return users;
    
}
//이곳에 @Repository가 등록되어야합니다.
public class UserDAO implements IUserDAO {

    @Autowired
    private JdbcTemplate template;
    
}

 

728x90
반응형