[Spring] 인프런 스프링 입문 강의 정리 #2

    본 블로그의 이미지 및 코드 등은

     

    https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8

     

     

    [지금 무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 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 : 작업 멈추기

     

    에러 메세지 : Syntax error in SQL statement "insert into member(name) [*]valuse('spring')"; expected "OVERRIDING USER VALUE, OVERRIDING SYSTEM VALUE, DIRECT, DEFAULT VALUES, VALUES, SET, WITH, (, SELECT, TABLE, VALUES"; SQL statement: insert into member(name) valuse('spring') [42001-224] 42001/42001

    SQL 구문 오류가 발생하였다.  valuseVALUES로 수정하면 문제를 해결했다. 

     

    강의에서는 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에 저장

    이후에는 DELETE FROM MEMBER로 데이터를 삭제해주었다.

     

    스프링 데이터 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);
    	}

    스프링 데이터 JPASpringDataJpaMemberRepository를 스프링 빈으로 자동 등록해 준다.

     

    어려워 죽것다...

    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");
    		}
    	}

    1. 회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항(핵심 비지니스 로직)이 아니다.
    2. 시간을 측정하는 로직은 공통 관심 사항이다.
    3. 시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지보수가 어렵다. 시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어렵다. -> 시작과 끝의 코드를 찾는 것이 어렵다.

    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 주입)되는 과정을 살펴보았다.

    실제 Proxy가 주입되는지 콘솔에 출력해서 확인하기

     

     


     

     

    1 실습 1 에러가 떠서 정말 힘들었다.

    오히려 에러가 떠서 기본적인 설정과 추가적인 공부를 할 수 있어서 좋았다고 생각할 수도 있나...?

    완전 럭키비키잖앙~🍀

     

    20분 강의를 3시간 동안 들은 만큼 끈질기게 파고들어서 그런가

    스프링에 조금씩 흥미가 생기고 있다🤩

     

    이상으로 스프링 인문편 강의 블로그를 마치겠다.

    'Java > Java Spring' 카테고리의 다른 글

    Spring MVC 개념과 패턴  (1) 2025.04.07
    [인프런] 스프링 핵심 원리 #1  (1) 2024.07.23
    [Spring] 인프런 스프링 입문 강의 정리 #1  (0) 2024.07.07

    댓글