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

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

    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 접근 기술] 강의를 바탕으로 작성한 글임을 밝힙니다. 

     


     

    백엔드 공부는 해야 하고,

    대체 어디서부터 시작해야 하는지 감이 안 잡혀서

    동아리원들끼리 백엔드 스터디를 만들었다!

     

    본 글은 [스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술]

    섹션 1부터 섹션 5까지 강의를 들으며 중요한 내용을 기억하기 위해 작성하였다.

     


    프로젝트 환경설정

    https://start.spring.io/

     

     

     

     

    VIEW SETTING

    사용자가 보낸 요청에 대해 웹 페이지가 생성되고 브라우저에 표시

    1. 웹 브라우저에서 localhost:8080/hello 를 요청

    2. 요청이 Spring Boot 애플리케이션에 내장된 톰캣 서버로 전달

    3. GetMapping이 hello를 캐치하여 helloController 호출

    4. ViewResolver에서 hello.html을 찾아 처리

     

     

     

    빌드하고 실행하기

    ./gradlew clean build				//build 폴더 완전히 지우고 다시 build
    cd libs						//build 폴더로 이동
    java -jar hello-spring-0.0.1-SNAPSHOT.jar	//파일 실행

     

     


     

     

    정적 콘텐츠(Static Content)

    프로그램 자체 수행

     

    1. 웹 브라우저에서 localhost:8080/hello-static.html 입력

    2. 내장 톰켓 서버요청받음

    3. Controller에서 먼저 탐색(우선순위) -> hello-static 컨트롤러 없음

    4. 내부 resource에서 hello-static.html 반환

     

     

     

    MVC와 템플릿 엔진

    MVC : Model, View, Controller

    Copy path-Absolute path 를 바로 입력하여 볼 수 있음 

    ? 는 && 의미로 name 값을 지정

     

    1. 웹 브라우저에서 localhost:8080/hello-mvc 입력

    2. 내장 톰켓 서버스프링 부트요청을 넘김

    3. hello-mvc가 helloController에 mapping이 되어있음 있으므로 return시 hello-template 이름을, 모델은 spring!!!!!! 넘겨줌

    4. ViewResolver(뷰를 찾아주고 화면에 표시)가 hello-template.html을 HTML 변환

     

     

     

    API

    commad+n -> Getter and Setter(자바 빈 표준 방식)

    public String getName() {
    			return name;
    		}
    
    		public void setName(String name) {
    			this.name = name;
    		}

     

    `@ResponseBody`를 사용하고, 객체를 반환하면 객체가 JSON으로 변환됨

    1. 웹 브라우저에서 localhost:8080/hello-api 입력

    2. 내장 톰켓 서버 스프링 부트 요청을 넘김

    3. helloController에서 hello-api 확인,

        @ResponseBody라는 애노테이션이 있으므로 http의 body에 직접 정보를 넘겨야겠다고 판단

    • String이면 StringConverter(StringHttpMessageConverter)
    • 객체 JsonConverter(MappingJackson2HttpMessageConverter)

    4. JSON을 웹 브라우저/서버에 보내줌

     

     


     

     

    비즈니스 요구사항

    • 컨트롤러 : 웹 MVC의 컨트롤러 역할
    • 서비스 : 핵심 비즈니스 로직 구현
    • 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장/관리
    • 도메인 : 비즈니스 도메인 객체(ex. 회원, 주문 등을 DB에 저장/관리)

    아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 설계

     

    회원 리포지토리

    option+enter : implements method

    Public class MemoryMemberRepository implements MemberRepository {
    
    	private static Map<Long, Member> store = new HashMap<>();
    	private static long sequence = 0L;
    
    	@Override
    	public Member save(Member member) {
    		member.setId(++sequence);
    		store.put(member.getId(), member);
    		return member;
    	}
    
    	@Override
    	public Optional<Member> findById(Long id) {
    		return Optional.ofNullable(store.get(id));	
            //값이 null인 경우 클라이언트에서 행동할 수 있음
    	}
    
    	@Override
    	public Optional<Member> findById(String name) {
    		return store.values().stream() //돌림
    			.filter(member -> member.getName().equals(name))
    			.findAny(); 
                //맵에서 하나 찾아지면 반환, 끝까지 돌렸는데 없으면 OptionalNull이 포함되어 반환
    	}
    
    	@Override
    	public List<Member> findAll() {
    		return new ArrayList<>(store.values());
    	}
    }

     

    main 메서드를 통해 실행하거나 컨트롤러를 통해 해당 기능을 시도하기에는 번거롭다.

    따라서 JUnit이라는 프레임워크로 테스트를 실행해서 기능을 테스트한다. 

     

    테스트 케이스는

    1. given : 어떤 상황이 주어졌을 때(실행)
    2. when : 실행했을 때(실행)
    3. then : 검증한 결과가 이게 나와야 함(확인)

    3단계구성되는 것이 좋다. 

    class MemoryMemberRepositoryTest {
    	MemoryMemberRepository repository = new MemoryMemberRepository();
        
    	@AfterEach
    	public void afterEach() {	//메모리 DB에 저장된 데이터를 삭제
    		repository.clearStore();
    	}
        
    	@Test
    	public void save() {
    		//given
    		Member member = new Member();
    		member.setName("spring");
            
    		//when
    		repository.save(member);
            
    		//then
    		Member result = repository.findById(member.getId()).get();
    		assertThat(result).isEqualTo(member);
    	}
        
    	@Test
    	public void findByName() {
    		//given
    		Member member1 = new Member();
    		member1.setName("spring1");
    		repository.save(member1);
    		Member member2 = new Member();
    		member2.setName("spring2");
    		repository.save(member2);
            
    		//when
    		Member result = repository.findByName("spring1").get();
            
    		//then
    		assertThat(result).isEqualTo(member1);
    	}
        
    	@Test
    	public void findAll() {
    		//given
    		Member member1 = new Member();
    		member1.setName("spring1");
    		repository.save(member1);
    		Member member2 = new Member();
    		member2.setName("spring2");
    		repository.save(member2);
            
    		//when
    		List<Member> result = repository.findAll();
            
    		//then
    		assertThat(result.size()).isEqualTo(2);
    	}
    }

    테스트는 각각 독립적으로 실행되어야 한다. 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니다.

     

    /Users/seoin/hello-spring/src/main/java/hello/hello_spring/repository/
    MemoryMemberRepository.java:45 java: class, interface, enum, or record expected

    오류 메시지 class, interface, enum, or record expected는 일반적으로 파일 끝에 불필요한 문자가 있거나 클래스, 인터페이스, 열거형 또는 레코드 정의 외부에 잘못된 구문이 있을 때 발생한다.

    나는 테스트가 계속 실패했었는데, 알고 보니 repository에 중괄호가 하나 더 있었다.  찾느라 꽤 애를 먹었다T0T

     

     

     

    회원 서비스

    command+m->Extract Method

    public class MemberService {
    
    	private final MemberRepository memberRepository = new MemoryMemberRepository();
    
    	/** 회원가입 **/
    	public Long join(Member member) {
    		//같은 이름이 있는 중복 회원X
    		validateDuplicateMember(member);	//중복 회원 검증
    		memberRepository.save(member);
    		return member.getId();
    	}
    
    	private void validateDuplicateMember(Member member) {
    		memberRepository.findByName(member.getName())
    			.ifPresent(m-> {
    				throw new IllegalStateException("이미 존재하는 회원입니다.");
    			});
    	}
    	
    	/** 전체 회원 조회 **/
    	public List<Member> findMembers() {
    		return memberRepository.findAll();
    	}
    	
    	public Optional<Member> findOne(Long memberID) {
    		return memberRepository.findById(memberID);
    	}
    }

     

    command+shift+T : create test

    • 테스트는 한글로 바꿔도 됨 
    • build 될 때 테스트 코드는 포함 X
    class MemberServiceTest {
    
    	MemberService memberService;
    	MemoryMemberRepository memberRepository;
    
    	@BeforeEach		//각 테스트 실행 전에 호출
    	public void beforeEach() {
    		memberRepository = new MemoryMemberRepository();
    		memberService = new MemberService(memberRepository); //DI
    	}
    
    	@AfterEach
    	public void afterEach() {
    		memberRepository.clearStore();
    	}
    
    	@Test
    	void 회원가입() {
    		//Given
    		Member member = new Member();
    		member.setName("hello");
    		//When
    		Long saveId = memberService.join(member);
    		//Then
    		Member findMember = memberRepository.findById(saveId).get();
    		assertEquals(member.getName(), findMember.getName());
    	}
    
    	@Test
    	public void 중복_회원_예외() throws Exception {
    		//Given
    		Member member1 = new Member();
    		member1.setName("spring");
    		Member member2 = new Member();
    		member2.setName("spring");
    		//When
    		memberService.join(member1);
    		IllegalStateException e = assertThrows(IllegalStateException.class,
    			() -> memberService.join(member2));//예외가 발생해야 한다. assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    	}
    }

     

     

     


     

     

     

     

    컴포넌트 스캔과 자동 의존관계 설정

    스프링 빈을 등록하는 방법

    • 컴포넌트 스캔(@이용)과 자동 의존관계(@Autowired) 설정
    @Controller
    public class MemberController {
    
    	//private final MemberService memberService = new MemberService();
    	//이렇게 등록하는 건 별로임; MemberService에는 별 기능이 없음. 공유해놓고 쓰면 되기 떄문
    	private final MemberService memberService;
    
    	@Autowired 		//스프링 컨테이너와 연결시켜줌 > 의존성 주입
    	public MemberController(MemberService memberService) {
    		this.memberService = memberService;
    	}
    }

     

    • 자바 코드로 직접 스프링 빈 등록하기
    @Configuration
    public class SpringConfig {
    
    	@Bean	//MemberService를 호출해서 빈으로 등록해줌
    	public MemberService memberService() {
    		return new MemberService(memberRepository());
    	}
    
    	@Bean
    	public MemberRepository memberRepository() {
    		return new MemoryMemberRepository();
    	}
    }

    `

    @Component`를 포함하는 다음 애노테이션도 스프링 빈으로 자동 등록된다. `

    • @Controller
    • @Service
    • @Repository

    @Autowired : 외부에서 객체를 생성해서 의존성 주입, DI(Dependency Injection)

     

    memberService` 와 `memberRepository` 가 스프링 컨테이너에 스프링 빈으로 등록

    같은 스프링 빈이면 모두 같은 인스턴스다.

     

    DI에는 필드 주입, setter 주입, 생성자 주입 이렇게 3가지 방법이 있다.

    의존관계가 실행 중에 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장

    //생성자 주입
    @AutoWired
    public MemberController(MemberService memberService) {
    		this.memberService = memberService;
    	}
        
    // 필드 주입, 안좋음
    @AutoWired private MemberService memberService;	
        
    //setter 주입, public하게 노출됨
    @AutoWired
    public void setMemberService(MemberService memberService) {
    		this.memberService = memberService;
    	}

     

     


     

    동아리 세션 때 이 강의를 미리 듣고 갔으면 훨씬 이해하기 편했을 거 같다. 

    과거 교육받을 때의 개념들이 강의 때 나와서, '아 이거 전에 들었던 거다!'라고 생각나서 반가웠다. 

     

    너무 내용이 길어지는 것 같아,

    섹션 6부터 9까지 강의는 다음 블로그에 작성하겠다:>

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

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

    댓글