본 블로그의 이미지 및 코드 등은
[지금 무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의 | 김영한 - 인프
김영한 | 스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., 스프링 학습 첫 길잡이! 개발 공부의 길을 잃지 않도록 도와드립니다. 📣 확
www.inflearn.com
[스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술] 강의를 바탕으로 작성한 글임을 밝힙니다.
회원 웹 기능
홈 컨트롤러 추가
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
}
컨트롤러가 정적 파일보다 우선순위가 높기 때문에, index.html에 쓴 hello라고 뜨지 않는 것이다.
H2 데이터베이스

iterm에서 h2 실행시키는 방법
h2 폴더-bin-h2.sh 실행
iterm에서
- control+z : 작업 멈추기
SQL 구문 오류가 발생하였다. valuse를 VALUES로 수정하면 문제를 해결했다.
강의에서는 insert into member(name) values('spring'); 으로 했는데 왜 나는 오류가 생겼는지 궁금하다0.0
순수 Jdbc
build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가
💡 포트 8080 사용 문제
Error starting ApplicationContext.
To display the condition evaluation report re-run your application with 'debug' enabled.
포트 8080이 이미 사용 중이어서 Spring Boot 애플리케이션이 시작되지 못한 문제가 발생하였다.
1. lsof -i :8080 //포트 8080을 사용 중인 프로세스를 찾는다.
2. kill -9 PID //포트 8080을 사용중인 프로세스를 찾아 종료시킨다.
포트 8080을 사용중인 프로세스를 강제 종료시켜 에러를 해결하였다.
🔥 option+enter : implement methods < 진짜 자주 쓰니까 꼭 기억해 두기
DB에 붙으려면 datasource가 있어야 함
@Override
public Member save(Member member) {
String sql = "insert into member (name) values (?)";
Connection conn = dataSource.getConnection(); //conn = connection
PreparedStatement pstmt = conn.prepareStatement(sql); //pstmt = prepareStatement
pstmt.setString(1, member.getName());
pstmt.executeUpdate();
return null;
}
이렇게 하면 DB에 쿼리가 전송된다.
import javax.sql.DataSource;
@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 MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
}
}
DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다.
스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둔다. 그래서 DI를 받을 수 있다.
💡 500 에러
localhost:8080의 회원가입과 회원목록에 접속하면 아래와 같은 에러가 발생하였다.
2024-07-10T00:25:49.316+09:00 ERROR 2804 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.IllegalStateException: org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "MEMBER" not found (this database is empty); SQL statement: select * from member [42104-224]] with root cause
Spring Boot에서 발생한 오류는 MEMBER 테이블을 찾을 수 없다는 것이다.
이는 데이터베이스가 비어 있거나 MEMBER 테이블이 생성되지 않았기 때문에 발생한다.
H2 콘솔에서 JDBC를 연결했을 때 비로소 오류가 해결되었다.
아직까지 H2콘솔 다루는 방법이나 데이터베이스에 대한 지식이 미흡한 것 같다.
구현 클래스 설명
- MemberService는 MemberRepository 의존
- MemberRepository는 MemoryMemberRepository와 JdbcMemberRepository를 구현체로 가지고 있음
스프링 컨테이너에서는 memory로 memberRepository를 스프링 빈으로 등록한 걸 해제하고,
Jdbc 버전의 memberRepository 등록
> 개방-폐회 원칙(OCP, Open-Closed Principle)
: 확장에는 열려있고, 수정/변경에는 닫혀있다.
기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다. -> 스프링 DI(Dependencies Injection) 사용
주의! 이렇게 JDBC API로 직접 코딩하는 것은 20년 전 이야기이다. 따라서 고대 개발자들이 이렇게 고생하고 살았구나 생각하고, 정신건강을 위해 참고만 하고 넘어가자.
JDBC API로 스프링 실습을 학습하고 나서, 얼마나 스프링이 편리한지 몸소 깨닫게 되었다.
정말 1 실습 1 에러가 떠서 힘들었다 T0T
스프링 통합 테스트
DB에 member 지우기 : DELETE FROM MEMBER
@SpringBootTest
@Transactional // 테스트 끝나면 롤백, DB데이터 삭제
class MemberServiceIntegrationTest {
@Autowired MemberService memberService; // 필드 기반으로 받음
@Autowired MemberRepository memberRepository;
- @SpringBootTest: 스프링 컨테이너와 테스트를 함께 실행한다.
- @Transactional: 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.
순수 JdbcTemplate
- 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다.
- SQL은 직접 작성해야 한다.
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
//@Autowired :생성자가 딱 하나만 있으면 오토와이어를 생략할 수 있다.
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
jdbcInsert.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;
};
}
}
JPA
JPA는 글로벌적으로 많이 사용하는 기술이다.
- JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.
JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있다. - JPA를 사용하면 개발 생산성을 크게 높일 수 있다.
스프링 부트에 JPA 추가 설정
spring.jpa.show-sql=true //show-sql: PA가 생성하는 SQL을 출력
spring.jpa.hibernate.ddl-auto=none
//ddl-auto: JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 `none` 를 사용하면 해당 기능을 끈다.
jpa는 인터페이스, Hibernate는 라이브러리 -> jpa 인터페이스에 Hibernate만 거의 쓴다고 생각
JPA = 객체 + ORM
*ORM: 객체 지향 프로그래밍 언어를 사용하여 호환되지 않는 유형의 시스템 간에 데이터를 변환하는 프로그래밍 기술
Object Relational Mapping: 객체의 object와 relational 데이터베이스의 table을 mapping
💡 jakarta.persistence 설치
import jakarta.persistence javax 파일이 없어서 자꾸 빨간 줄이 뜨는 문제가 있었다.
https://mvnrepository.com/artifact/javax.persistence/javax.persistence-api/2.2 에서 jar 파일을 받아 설치하고,
implementation 'javax.persistence:javax.persistence-api:2.2'
build.gradle에 코드를 추가하여 문제를 해결하였다.
command+T : inline
List<Member> result = em.createQuery("select m from Member m", Member.class).getResultList();
return result;
//command+T
return em.createQuery("select m from Member m", Member.class).getResultList();
JPA 엔티티 매핑
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
JPA 회원 리포지토리
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
public Member save(Member member) {
em.persist(member);
return member;
}
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class).getResultList();
}
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();
} }
서비스 계층에 트랜잭션 추가
import org.springframework.transaction.annotation.Transactional
@Transactional
public class MemberService {}
- 스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋한다. 만약 런타임 예외가 발생하면 롤백한다.
- JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.
💡 org.hibernate.HibernateException
Caused by: org.hibernate.HibernateException: Unable to determine Dialect without JDBC metadata
에러가 발생하면서 localhost:8080도 접속이 되지 않았다.
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
결론적으로는 application.properties에 을 추가하여 문제를 해결하였다. 테스트 코드도 함께 문제 해결이 돼서 놀랐다.
강의에서는 저 코드를 추가하지 않았는데 왜 해결되었는지 잘 모르겠다.
테스트 케이스
@SpringBootTest
class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
@Commit
void 회원가입() {
//Given
Member member = new Member();
member.setName("에러 많이 떠서 슬프다...");
//When
Long saveId = memberService.join(member);
//Then
Member findMember = memberRepository.findById(saveId).get();
assertEquals(member.getName(), findMember.getName());
}
- @Commit: 작업 내용을 영구적으로 DB에 저장
스프링 데이터 JPA 회원 리포지토리
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
Optional<Member> findByName(String name);
}
스프링 데이터 JPA 회원 리포지토리를 사용하도록 스프링 설정 변경
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
스프링 데이터 JPA가 SpringDataJpaMemberRepository를 스프링 빈으로 자동 등록해 준다.

AOP
AOP가 필요한 상황
- 모든 메소드의 호출 시간을 측정하고 싶다면?
- 공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern)
- 회원 가입 시간, 회원 조회 시간을 측정하고 싶다면?
/** 회원가입 **/
public Long join(Member member) {
long start = System.currentTimeMillis();
try {
validateDuplicateMember(member); //중복 회원 검증
memberRepository.save(member);
return member.getId();
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("join = " + timeMs + "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");
}
}
- 회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항(핵심 비지니스 로직)이 아니다.
- 시간을 측정하는 로직은 공통 관심 사항이다.
-
시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지보수가 어렵다. 시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어렵다. -> 시작과 끝의 코드를 찾는 것이 어렵다.
AOP: Aspect Oriented Programming
공통 관심 사항(cross-cutting concern)과 핵심 관심 사항(core concern)을 분리
@Aspect
@Component
public class TimeTraceAop {
@Around("execution(* hello.hello_spring..*())")
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");
}
}
}
회원 가입, 회원 조회 같은 핵심 관심 사항과 시간을 측정하는 공통 관심 사항을 분리하였다.
memberController가 호출하면, 실제 memberService가 호출되는 것이 아니라 proxy로 발생하는 가짜 memberService를 이용한다.
System.out.println("memberService = " + memberService.getClass());
memberController에 코드를 추가하여 인젝션(SQL 주입)되는 과정을 살펴보았다.
1 실습 1 에러가 떠서 정말 힘들었다.
오히려 에러가 떠서 기본적인 설정과 추가적인 공부를 할 수 있어서 좋았다고 생각할 수도 있나...?
완전 럭키비키잖앙~🍀
20분 강의를 3시간 동안 들은 만큼 끈질기게 파고들어서 그런가
스프링에 조금씩 흥미가 생기고 있다🤩
이상으로 스프링 인문편 강의 블로그를 마치겠다.
'Java > Java Spring' 카테고리의 다른 글
Spring MVC 개념과 패턴 (1) | 2025.04.07 |
---|---|
[인프런] 스프링 핵심 원리 #1 (1) | 2024.07.23 |
[Spring] 인프런 스프링 입문 강의 정리 #1 (0) | 2024.07.07 |
댓글