2024.09.06.금.TIL JDBC, SQL Mapper, ORM

김기남·2024년 9월 6일
post-thumbnail

안녕하세요, 오늘은 JDBC, SQL Mapper, ORM에 대해 알아보았습니다.

JDBC의 한계와 발전

  1. 간단한 SQL을 실행하는 경우에도 중복된 코드가 너무 많았다.
  2. DB에 따라 일관성 없는 정보를 가진 채로 Checked Exception(SQL Exception) 처리를 한다.
  3. Connection과 같은 공유 자원을 제대로 반환하지 않으면 한정된 시스템 자원(CPU, Memory)에 의해 문제가 발생한다.
  4. SQL Query를 일일히 작성하여야 합니다.

Persistence Framework

💡 JDBC 처럼 복잡함이나 번거로움 없이 간단한 작업만으로 Database와 연동되는 시스템을 개발하게 되었다. 이것을 Persistence Framework 라고 한다.
  • 모든 Persistence Framework는 내부적으로 JDBC API를 이용한다.
    → preparedStatement
    → 위에서 JDBC API를 설명한 이유.
  • 크게 SQL Mapper, ORM 두가지로 나눌 수 있다.

JDBC, SQL MAPPER, ORM의 공통점

💡 영속성(Persistence)

데이터를 생성한 프로그램의 실행이 종료되더라도
사라지지 않는 데이터의 특성, 영구히 저장

SQL Mapper

💡 직접 작성한 SQL 문의 실행 결과와 객체(Object)의 필드를 Mapping하여 **데이터를 객체화한다.**

Spring JDBC Template

💡 Spring Framework에서 제공하는 JDBC 작업을 단순화하고 개선한 유틸리티 클래스

JDBC Template의 장점

  1. 간편한 데이터베이스 연결 → 손수 적었던 연결 코드들을 yml, properties
  2. PreparedStatementCallableStatement를 처리한다.
  3. 예외 처리와 리소스 관리 - DB Connection을 자동으로 처리하여 리소스 누수 방지
  4. 결과 집합 처리 - 데이터를 자바 객체로 변환할 수 있도록 돕는다.
  5. 배치 처리 작업을 지원한다. - Spring Batch → 매일 동일한 시간에 수행되는 쿼리

예시코드

// 1. XML OR Gradle에 Spring JDBC 의존성 추가
// 2. application.properties OR application.yml에 데이터베이스 연결 설정

@RestController
public class MemberController {
    private final MemberRepository memberRepository;

    public MemberController(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @GetMapping("/members")
    public List<Member> getAllMembers() {
        return memberRepository.getAllMembers();
    }
}

// Member Object
public class Member {
    private Long id;
    private String name;
    private int age;

    // Getter and Setter methods
}

// Repository Anotation의 역할에 대해 공부해주세요.
@Repository
public class MemberRepository {
    private final JdbcTemplate jdbcTemplate;

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

		// Member 객체로 리턴한다.
    public List<Member> getAllMembers() {
        String query = "SELECT * FROM MEMBER";
        return jdbcTemplate.query(query, (rs, rowNum) -> {
            Member member = new Member ();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            member.setAge(rs.getInt("age"));
            return member;
        });
    }
}

Mybatis

💡 반복적인 JDBC 프로그래밍을 단순화 시켜줍니다. SQL 쿼리들을 XML 파일에 작성하여 코드와 SQL을 분리하여 관리되도록 합니다. **SQL과 Java Code의 분리**가 핵심

Query를 JAVA에서 XML로

  • 복잡한 JDBC 코드가 사라집니다.
  • ResultSet과 같이 결과값을 맵핑하는 객체가 없다.
  • 설정이 간단합니다.
  • 관심사를 분리합니다. SQL 을 따로 관리한다.
  • XML 안에있는 SQL을 Java의 메소드에 매핑해준다.

MyBatis 장점

  1. 자동으로 Connection 관리를 해주면서 JDBC 사용할 때의 중복 작업 대부분을 없애준다.
  2. DB 결과 집합을 자바 객체로 매핑할 수 있다.
  3. 복잡한 쿼리나 다이나믹하게(동적쿼리) 변경되는 쿼리 작성이 쉽다.
  4. 관심사 분리 - DAO로부터 SQL문을 분리하여 코드의 간결성 및 유지보수성이 향상된다.
  5. 쿼리 결과를 캐싱하여 성능을 향상시킬 수 있다.

Mybatis는 동적 쿼리를 지원한다.
동적 쿼리(Dynamic SQL)란?
상황에 따라 분기처리(IF)를 통해 쿼리를 동적으로 만들어주는것.

MyBatis 예시코드

// User 클래스
public class User {
    private Long id;
    private String userName;
    private String email;

    // Getter and Setter methods
}



// Mapper Interface 생성 : SQL 쿼리와 매핑을 정의하는 인터페이스
public interface UserMapper {
    User getUserById(Long id);
}


public class Main {
    public static void main(String[] args) {
        String resource = "mybatis-config.xml";
        try (Reader reader = Resources.getResourceAsReader(resource)) {
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
            try (SqlSession session = sqlSessionFactory.openSession()) {
                UserMapper userMapper = session.getMapper(UserMapper.class);
                User user = userMapper.getUserById(1L);
                System.out.println(user.getId() + ", " + user.getUsername() + ", " + user.getEmail);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Mapper XML

SQL 쿼리와 객체 매핑을 정의한다.

<mapper namespace="com.example.UserMapper">
    <select id="getUserById" resultType="com.example.User">
        SELECT id, userName, email
        FROM users
        WHERE id = #{id}
    </select>
</mapper>

MyBatis 설정 파일 작성

<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC" />
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://localhost:3306/mydb" />
                <property name="userName" value="userName" />
                <property name="password" value="password" />
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/example/UserMapper.xml" />
    </mappers>
</configuration>

SQL Mapper의 한계

  1. SQL을 직접 다룬다.
  2. 특정 DB에 종속적으로 사용하기 쉽다.
    (DB마다 Query문이 조금씩 다름, 함수도 다름)
  3. 테이블마다 비슷한 CRUD SQL = DAO 개발이 반복된다. → 코드 중복
    DAO(Data Access Object)
  4. 테이블 필드가 변경될 시 이와 관련된 모든 DAO의 SQL문, 객체의 필드 등을 수정해야 한다.
    코드상으로 SQL과 JDBC API를 분리했지만 논리적으로 강한 의존성을 가지고 있다.
  5. 객체와의 관계는 사라지고 DB에 대한 처리에 집중하게 된다.
    SQL 의존적인 개발

관계형 DB와 객체지향의 패러다임 불일치

💡 객체지향으로 설계된것을 관계형 DB에 저장하기란 어렵습니다. 또한, 테이블에 저장한 데이터를 다시 객체화 하는데에도 어려움이 생깁니다.
public class Member {
	// 필드들..
	// 객체 안의 객체
	private Team team;
} -> ERD?

public class Member extends Person {
	// 필드들..
} -> ERD?

public class Member extends Person implements Workable {
  // 필드들..
} -> ERD?
  • 객체지향 : 추상화, 상속, 다형성 → 객체 중심
  • 관계형 데이터베이스(RDB) → 데이터 중심의 구조

각각 지향하는 목적이 다르기 때문에 사용 방법과 표현 방식에 차이가 있다.
패러다임 불일치 문제를 해결하기 위해 ORM이 등장하게 된다.

ORM(Object Relational Mapping)

💡 객체와 관계형 DB를 매핑한다. SQL Query가 아닌 메서드로 데이터 조작 ex) `SELECT * FROM USER;`→ user.findAll();

ORM의 장점과 단점

장점

  1. 패러다임 불일치 문제 해결 - 객체지향 언어가 가진 장점을 활용 가능
  2. 생산성 - 반복적인 CRUD SQL을 개발자가 직접 작성하지 않아도 된다
  3. 데이터 접근(DB 종속성) 추상화 - DB마다 조금씩 다른 타입, SQL Query 문법을 손쉽게 해결한다.
    ex) MySQL, ORACLE 바뀌어도 코드는 일치하기 때문에 상관없음.
  4. 유지보수성 - 필드 추가, 삭제 시 관련된 SQL 쿼리를 직접 수정하지 않고 Entity만 수정하면 된다.
  5. SQL Mapper나 JDBC와 다르게 컴파일 단계에 쿼리에 대한 에러를 알 수 있다.

단점

  1. 복잡한 쿼리 사용이 아주 어렵다.
    JPQL을 지원한다.
    → QueryDSL 을 함께 사용한다.

JPA

JPA는 JAVA ORM에 대한 API 표준 명세이다.
인터페이스로 구성되어 있으며 사용하려면 ORM 프레임워크를 사용하여야 합니다.

  • JPA에는 HIBERNATE, EclipseLink, DataNucleus라는 구현체가 있습니다.
  • 여기서 가장 대표적인 프레임워크가 하이버네이트

패러다임 불일치 - 상속

Person 추상 클래스를 상속받는 Member 클래스

abstract class Person {
	private Long id;
	private String name;
}

public class Member extends Person {
	// private Long id;
	// private String name;
	private String nickName;
}

관계형 데이터베이스에는 상속이라는 개념이 없다.

Member 객체를 저장하고자 한다면?

INSERT INTO person 필드들..
INSERT INTO member 필드들..

총 Query를 두번 실행해야 한다.

Member 객체를 조회하려면?

SELECT p.*, m.*
FROM person p
JOIN member m
ON p.id = m.id;

JOIN문을 활용하여 조회하여야 한다.

만약 더 복잡한 상속관계가 있다면?

Query문 또한 점점 늘어나고 복잡하게 만들어진다.
이러한 상속에 의한 패러다임 문제는 JPA를 통해 쉽게 해결이 가능하다.

// INSERT INTO person 필드들.. , INSERT INTO member 필드들..
Member member = new Member();
jpa.persist(member); // JPA save()

// SELECT p.*, m.* 
// FROM person p 
// JOIN member m 
// ON p.id = m.id;
Member member = jpa.find(Member.class, memberId); // JPA findById(memberId);

패러다임 불일치 - 연관관계

💡 객체는 참조(주소값)를 사용해 다른 객체와 연관관계를 가집니다. 테이블은 외래키(FK)를 사용해 다른 테이블과 연관관계를 가집니다.

데이터 중심 설계
Team, Member 클래스

public class Member {
	private Long id; // Member PK
	private String nickName;

	private Long teamId; // Team FK
}

public class Team {
	private Long id; // Team PK
	private String name;
}

Team team = member.getTeam(); // ??

RDB에서의 구조

만약 객체를 테이블 연관관계에 맞추어 모델링하면
저장, 조회는 편리하겠지만 객체지향의 특징을 사용하지 못합니다.

객체지향 설계

객체간의 연관성에 대한 문제는 해결되지만,
테이블 기준으로 본다면 저장하거나 조회하는것이 복잡해지게 된다.
ex) INSERT Query가 두번 실행되야한다.

public class Member {
	private Long id;
	private String nickName;
	private Team team;
}

public class Team {
	private Long id;
	private String name;
}

JPA는 객체의 참조로 연관 관계 저장을 가능하도록 만들어준다.

member.setTeam(team);
jpa.persist(member); // INSERT 2 개.

Member member = jpa.find(Member.class, id);
Team team = member.getTeam();

개발자는 Member와 Team 관계를 설정하고 객체를 저장하기만 하면 된다.

패러다임 불일치 - 객체 그래프 탐색

💡 객체에서 Member가 소속된 Team을 조회할때는 참조를 사용하여 연관된 Team을 찾으면 되는데 이것을 **객체 그래프 탐색**이라고 한다.

객체내에서는 그래프 탐색을 자유롭게 할 수 있지만,
SQL을 다룰 때는 작성하는 쿼리에따라 객체의 그래프 탐색의 범위가 제한된다.

SQL 사용시 객체의 그래프 탐색 범위 제한

직접 구현한 DAO에서 객체를 가져온 경우
반환받은 엔티티를 신뢰하고 사용할 수 없다.

public void getStationByLineId(Long lineId) {
	Line line = lineDAO.findLine(lineId); // findSection() findStation()
	line.getSection().getStation(); // ?? 이게 신뢰할 수 있나? + 계속 작성해야하나?
}
  1. 노선으로 구간을 탐색하려고 한다. (DAO에 노선으로 구간을 탐색하도록 조회 SQL을 작성한다.)
  2. 노선에서 → 구간 → 역을 탐색하려고 한다.
    (모든 구간에 역을 조회할 수 있도록 SQL, DAO를 수정 해야함)
    만약 구간이 100개라면..?
    어디까지 탐색이 가능한지 알아보려면? 모든 DAO, SQL을 열어봐야함

결론

엔티티가 SQL에 논리적으로 종속되어서 문제가 발생한다.
온전히 객체만으로 동작을 파악하기 힘들다.
Line과 연관된 모든 객체 그래프를 조회해서 메모리에 올려두는 것은 자원의 낭비가 심하다.

이러한 문제를 JPA에서는 지연로딩으로 해결한다.

지연로딩

💡 객체가 참조하는 연관객체가 있을 경우 해당 객체의 조회를 미루고 있다가 사용할 때 조회하는것을 말한다.
public void lazyLoading(Long lineId) {
	// Line만 조회하는 sql문이 실행된다.
	Line line = lineDao.findById(lineId);
	
	// 아래 코드가 실행되는 시점에 section 데이터를 조회한다. 
	Section section = line.getSection.get(0);

	// 아래 코드가 실행되는 시점에 station 데이터를 조회한다. 
	String stationName = section.getStation().getName();
}

JPA에서는 연관된 객체를 즉시 조회할지 사용되는 시점에 지연로딩을 사용하여 조회할지
설정만으로 정의하여 성능 최적화에 이용하기도 한다.

ex) 10000번 조회하는데 10000회 연관객체를 함께 조회한다면?

Member 조회 할때, 항상 → 연관관계를 통해 Company 테이블을 조회한다면

즉시로딩 = 항상 Mebmer를 조회할때 Company같이 조회되도록 만드는것.

지연로딩 = Member는 Member를 조회할 때 , Company는 Company 조회할 때 쿼리가 실행되게 만듬

패러다임 불일치 - 비교

💡 DataBase는 PK(id값)로 각 row를 구분한다. 객체는 **동일성** Identify(인스턴스 주소 값) 비교와 **동등성** Equality(객체 내부의 값을 비교) 비교로 구분한다.
public class MemberDAO {
	public Member findMemberById(Long memberId) {
		// "SELECT * FROM MEMBER WHERE MEMBER_ID = ?
		// JDBC API를 실행 
		return member;
	}
}

동일성 비교

// 조회한 회원 비교하기
String memberId = "100";
Member member1 = memberDAO.findMemberById(memberId);
Member member2 = memberDAO.findMemberById(memberId);
member1 == member2; // false 다르다.

member1과 member2는 같은 DB Row에서 조회했지만
객체 측면에서 바라보면 둘은 다른 인스턴스이다.

JPA에서 동일성 비교

같은 트랜잭션 내에서 같은 아이디로 같은 객체가 조회되는 것을 보장한다.

// 조회한 회원 비교하기
String memberId = "100";
Member member1 = jpa.findMemberById(Member.class, memberId);
Member member2 = jpa.findMemberById(Member.class, memberId);

member1 == member2; // True 같다.

결론

ORM을 사용함으로써 우리는
데이터베이스 서버가 없는 상태에서도 개발이 가능하도록 되었습니다.

→ ex) Entity, Contorller, Service, DTO, Repository
→ DB없이 개발이 가능하다.

과제 진행은 앞으로 모두 MySQL로 통일! → Database 연결 MySQL, MongoDB, Oracle

정리


JDBC → SQL Mapper(JDBC Template, Mybatis) → ORM(JPA Hibernate)

모두 JDBC API를 내부적으로 사용하고 있다.

SQL Mapper, ORM 들은 내부적으로 Prepared Statement를 사용하고있다.

SQL Mapper, ORM 들은 Database Connection등 리소스 관리를 자동으로 해주고있다.

ORM은 컴파일 에러가 발생한다. 객체지향 패러다임에 대응할 수 있다.

ORM을 이용하면 우리는 개발에 집중할 수 있다!

Insert 문이 몇번 나가고, join 몇번 나가고, SQL 작성하고, 테스트하고

개발자가 할일 = 연관관계 설정 끝. JPA가 알아서 다 해준다. 생산성 올라간다.

오늘의 코드카타

class Solution {

    public String solution(String bin1, String bin2) {
        String answer = "";
        int num1 = Integer.parseInt(bin1, 2);
        int num2 = Integer.parseInt(bin2, 2);
        int res = num1 + num2;
        answer = Integer.toBinaryString(res);
        return answer;
    }
}

이진수 더하기

profile
새로운 시작~!

0개의 댓글