[스프링 입문] 3

smj_716·2025년 1월 7일

스프링 완전 정복

목록 보기
3/16

1. 스프링 DB 접근 기술

1) 순수 JDBC

  • JDBC API로 직접 코딩하는 것은 아주 오래전 일이다. 따라서 예전에는 이렇게 했구나 생각하고 참고만 하기! (깃허브에 코드 있음)
  • 자바에서 DB와 연결하기 위한 기본적인 API이다. ⚠️ 하지만 많은 반복 코드와 예외 처리를 필요로 하며 확장성과 유지보수가 어렵다.

2) 스프링 JdbcTemplate

순수 JDBC의 반복 코드를 줄여주지만 SQL 작성은 여전히 필요하다.

  • 데이터 접근을 위한 템플렛 제공
  • 간단한 CRUD는 효과적으로 처리 가능하지만 ⚠️복잡한 쿼리에서는 유지보수가 어려움

🖥️ MemberRepository

public class JdbcTemplateMemberRepository implements MemberRepository {
    
    private final JdbcTemplate jdbcTemplate;

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

    @Override
    public Member save(Member member) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate)
            .withTableName("member").usingGeneratedKeyColumns("id");
        Map<String, Object> parameters = new HashMap<>();
        parameters.put("name", member.getName());
        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
        member.setId(key.longValue());
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        List<Member> result = jdbcTemplate.query("SELECT * FROM member WHERE id = ?", memberRowMapper(), id);
        return result.stream().findAny();
    }

    @Override
    public List<Member> findAll() {
        return jdbcTemplate.query("SELECT * FROM member", memberRowMapper());
    }

    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = jdbcTemplate.query("SELECT * FROM member WHERE name = ?", memberRowMapper(), name);
        return result.stream().findAny();
    }

    private RowMapper<Member> memberRowMapper() {
        return (rs, rowNum) -> {
            Member member = new Member();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            return member;
        };
    }
}

🖥️ SpringConfig

@Configuration
public class SpringConfig {

    private final DataSource dataSource;

    public SpringConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new JdbcTemplateMemberRepository(dataSource);
    }
}

3) JPA

SQL을 직접 생성하지 않고, 객체 중심의 데이터베이스 접근을 가능하게 한다. SQL 대신 JPQL을 사용하며 ORM(Object-Relational Mapping)을 통해 엔티티와 테이블을 매핑한다.

  • 객체 중심 설계와 호환
  • P.K. 기반 단건 조회는 persist(), find() 등으로 처리 가능, 그 외 조회조건 주거나 다건 조회 등JPQL 작성해줘야 함

🖥️ MemberRepository

public class JpaMemberRepository implements MemberRepository {
    
    private final EntityManager em;

    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }

    @Override
    public Member save(Member member) {
        em.persist(member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }

    @Override
    public List<Member> findAll() {
        return em.createQuery("SELECT m FROM Member m", Member.class).getResultList();
    }

    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = em.createQuery("SELECT m FROM Member m WHERE m.name = :name", Member.class)
                                .setParameter("name", name)
                                .getResultList();
        return result.stream().findAny();
    }
}

🖥️ MemberService

 @Transactional
 public class MemberService {}
  • 서비스 계층에 @Transactional 추가 : 스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커
    밋함. 만약 런타임 예외가 발생하면 롤백
  • JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야함

트랜잭션은 데이터베이스 작업의 최소 단위, 여러 작업을 한 번에 처리하고 모두 성공해야만 최종 반영(커밋), 하나라도 실패하면 되돌림(롤백)

🖥️ SpringConfig

@Configuration
public class SpringConfig {

    private final EntityManager em;

    public SpringConfig(EntityManager em) {
        this.em = em;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new JpaMemberRepository(em);
    }
}

4) 스프링 데이터 JPA

JPA를 기반으로 더 높은 생산성을 제공한다.
repository 인터페이스만으로 기본적인 CRUD 및 쿼리를 구현할 수 있다.

  • 기본 CRUD 기능 제공
  • findByName(), findByEmail()처럼 메서드 이름만으로 기능 제공
  • 페이징 및 정렬 지원
  • JPA와 함께 사용해야함
  • 복잡한 동적 쿼리는 Querydsl 등 추가 라이브러리가 필요

🖥️ MemberRepository

public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    Optional<Member> findByName(String name);
}

🖥️ SpringConfig

@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

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

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository);
    }
}

👉 환경설정 (JPA와 스프링 데이터 JPA 동일)

[ 라이브러리 ]

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

[ application.properties ]

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
  • username=sa 추가하지 않으면 오류 발생
  • show-sql : JPA가 생성하는 SQL 출력함
  • ddl-auto : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none은 그 기능을 끈다. create를 사용하면 엔티티 정보를 이용해 테이블도 직접 생성해줌

2. AOP

1) AOP 필요한 상황

애플리케이션 개발 시, 모든 메서드에서 공통적으로 수행되는 작업은 핵심 비지니스 로직과 섞이면 코드가 복잡해지고 유지보수가 어려워진다.

예를 들어, 회원 가입조회 시 시간을 측정하는 코드를 추가하려면?

🖥️ MemberService

public Long join(Member member) {

    long start = System.currentTimeMillis();
    
    try {
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
        
    } finally {
        long finish = System.currentTimeMillis();
        System.out.println("join " + (finish - start) + "ms");
    }
}

public List<Member> findMembers() {

    long start = System.currentTimeMillis();
    
    try {
        return memberRepository.findAll();
        
    } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("findMembers " + timeMs + "ms");
    }
 }

⚠️ 문제

  • 회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항이 아님
  • 시간을 측정하는 로직은 공통 관심 사항
  • 시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지보수가 어려움
  • 시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어려움
  • 시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야함

2) AOP 적용

AOP를 사용하면 시간 측정과 같은 공통 관심 사항별도의 로직으로 분리하고, 핵심 비즈니스 로직을 깔끔하게 유지할 수 있다.

@Component
@Aspect
public class TimeTraceAop {

    @Around("execution(* hello.hellospring..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
    
        long start = System.currentTimeMillis();
        System.out.println("START: " + joinPoint.toString());
        
        try {
            return joinPoint.proceed();
        } 
        finally {
          long finish = System.currentTimeMillis();
          long timeMs = finish - start;
          
          System.out.println("END: " + joinPoint.toString()+ " " + timeMs + "ms");
        }
    }
}
  • @Aspect: AOP의 정의를 나타내는 어노테이션
  • @Around: 특정 메서드 실행 전후에 공통 로직을 수행
  • joinPoint.proceed(): 핵심 비즈니스 로직을 호출함
    시간 측정 로직은 AOP로 처리되므로 비즈니스 로직에서는 삭제 가능

💡 해결

  • 공통 로직 분리: 시간 측정과 같은 공통 로직을 하나의 클래스로 분리하여 관리할 수 있음
  • 비즈니스 로직의 간결화: 핵심 관심 사항에만 집중할 수 있음
  • 변경 용이성: 공통 로직 수정이 필요할 경우 AOP 클래스만 수정하면 됨
  • 적용 대상 선택 가능: 특정 패키지나 메서드에만 AOP를 적용할 수 있음

0개의 댓글