React + Spring - API 연동 (스프링편)

김태훈·2023년 1월 19일
0

자, React에서

client.post('/api/auth/register',{username,password});

이렇게 전송한 API를 스프링에서 어떻게 받을 것인가.

1. 나의 생각

  1. 회원 DOMAIN을 생성
  2. 레포지토리 생성
  3. 회원가입 / 로그인 로직을 다루는 회원 SERVICE 생성
  4. 프론트에서 넘어온 회원 정보 API를 Controller로 매핑해서 회원 도메인 객체 생성 및 서비스를 통하여 레포지토리에 저장

1. 회원 Domain 생성

package com.example.SpringAndReact.Domain;

public class Member {
    private Long id;
    private String userId;
    private String password;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

2. 회원 레포지 생성하기

회원 레포지를 만들기에 앞서서 먼저 인터페이스를 만들자.
OCP(개방 폐쇄), DIP(의존 관계 역전) 의 SOLID 원칙을 지키기 위해 노력해봅시다.

  • 인터페이스
package com.example.SpringAndReact.Repository;

import com.example.SpringAndReact.Domain.Member;

public interface MemberRepository {
    Member saveMember(Member member);
    Member findByUserId(Long id);
}
  • 구현체 (메모리)
    메모리에 저장하기 위해 그냥 Map이라는 자료구조를 써서 해봅시다.
package com.example.SpringAndReact.Repository;

import com.example.SpringAndReact.Domain.Member;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class MemoryMemberRepository implements MemberRepository {
    private static Map<Long,Member> store = new HashMap<>();
    private static long sequence = 0L;

    @Override
    public Member saveMember(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional<Member> findByUserId(String id) {
        return store.values().stream().filter(member->member.getUserId().equals(id)).findAny();
    }
}

3. 회원 서비스 생성하기 및 AppConfig 활용한 DIP문제 해결

  • 회원 서비스 인터페이스
package com.example.SpringAndReact.Service;

import com.example.SpringAndReact.Domain.Member;

public interface MemberService {
    void join(Member member);
}
  • 회원 서비스 구현체

아무래도 회원 서비스 클래스에서 로직을 짜려면 레포지토리를 건드려야 하는데,

private final MemberRepository memberRepository = new MemoryMemberRepository();

이렇게 짜버리면 SOLID 원칙의 OCP, DIP문제 두가지 모두 생길 수 있다. 따라서 AppConfig로 이를 해결해보자!


먼저 구현체에서 생성자 주입(DI) 방식을 활용하고, 주입은 AppConfig에게 맡긴다

  • 구현체
package com.example.SpringAndReact.Service;

import com.example.SpringAndReact.Domain.Member;
import com.example.SpringAndReact.Repository.MemberRepository;

public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;

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

    @Override
    public void join(Member member) {
        validateDuplicateMember(member);
        memberRepository.saveMember(member);
    }

    @Override
    public void validateDuplicateMember(Member member) {
        memberRepository.findByUserId(member.getUserId()).ifPresent(mem->{
            try{
                throw new IllegalAccessException("이미 존재하는 회원입니다.")
            }catch(IllegalAccessException e){
                throw new RuntimeException(e);
            }
        });
    }
}
  • AppConfig
package com.example.SpringAndReact;

import com.example.SpringAndReact.Repository.MemoryMemberRepository;
import com.example.SpringAndReact.Service.MemberService;
import com.example.SpringAndReact.Service.MemberServiceImpl;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    public MemberService memberService (){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
}

4. Controller에서 @postmapping으로 받아와서 서비스를 통하여 레포지토리에 저장

먼저 post로 받아온 정보를 받아와서 Member 도메인 객체로 지정하려면 API부터 처리한다.
해당 API 처리과정에서 쓸 MemberForm 클래스를 먼저 정의하자

package com.example.SpringAndReact.controller;

public class MemberForm {
    private String UserId;
    private String password;

    public String getUserId() {
        return UserId;
    }

    public void setUserId(String userId) {
        UserId = userId;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

그 후에,
form으로 들어온 정보를 @postmapping함수 내에서 다음과 같이 처리해보겠다.
마찬가지로 DI 를 이용한다.

RequestMapping으로 "api/auth" path를 먼저 받고,
그 후에 Request 별 Mapping을 해당 path에 추가시키는 형태로 받는다

package com.example.SpringAndReact.controller;

import com.example.SpringAndReact.Domain.Member;
import com.example.SpringAndReact.Service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
@RequestMapping("api/auth")
public class MemberController {
    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    @PostMapping("/register")
    @ResponseBody
    public Member createForm(@RequestBody MemberForm form){
        Member member = new Member();
        member.setUsername(form.getUsername());
        member.setPassword(form.getPassword());
        System.out.println(member.getUsername());
        System.out.println(member.getPassword());
        memberService.join(member);
        return member;
    }
};


후에, MemberService 관련 생성자 주입 과정을 AppConfig에 추가해주자.
하지만 이 코드는 동작되지 않는다!!!!
이미 Controller에서 memberService롤 autowired해주었기 때문이다. = 사실상 이미 Bean에 올라간 것과 같은 논리 (Controller 에서의 autowired는 필수불가결하다)
따라서 config에서 또다시 정의할 생각을 하지 말자.

package com.example.SpringAndReact;

import com.example.SpringAndReact.Repository.MemoryMemberRepository;
import com.example.SpringAndReact.Service.MemberService;
import com.example.SpringAndReact.Service.MemberServiceImpl;
import com.example.SpringAndReact.controller.MemberController;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService (){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
    @Bean
    public MemberController memberController() { 
    	return new MemberController(memberService());}
}

추가로, 리액트에서 프록시 설정을 바꾸면 무조건 리액트 서버를 열고 꺼야합니다.
즉, 페이지 렌더링하는것은 바로 새로고침되지만, 서버 설정은 서버를 열고 꺼야함!!
(프록시 설정을 https://localhost:8080으로 해서 안되었다가, https를 http로 바꾸면서 일어난 일)

2. 문제발생

@PostMapping("/register")
    public String createForm(@RequestBody MemberForm form){
        Member member = new Member();
        System.out.println(form.getUserId());
//        member.setUserId(form.getUserId());
//        member.setPassword(form.getPassword());
//        memberService.join(member);

        return "login";
    }

Controller에서 넘어온 data 의 form 을 확인해보았지만 null 이 찍힘 매우 슬프다..
그래서 먼저 axios 의 post가 잘되는지 체크해보았다.

export const register = ({username,password}) =>
    client.post('/api/auth/register',{username,password}).then((response)=>
        {console.log(response.data);}).catch((error)=>{console.log(error);});

왜 또 회원가입조차안되는거지..
그냥 원상태로 되돌렸다.. 는 무슨

3. 문제해결

해당 문제는 클라이언트에서 서버로 전송하는 form 태그의 post의 '이름'이 달라서 생긴 문제다.

API를 전송하는 lib/api 파일에

//회원 가입
export const register = ({username,password}) =>
    client.post('/api/auth/register',{username,password});

이렇게 적혀있다는 것은, username과 password를 전송하겠다는 뜻이다.

따라서, 서버에서 받을 때에도 해당 이름과 맞추어서 username과 password로 저장해야한다.

이 전에 문제에서는 password는 같아서 password는 제대로 받아졌지만, 내가 디버그를 password를 하지 않고, userId로 디버그를 해서 아예 API가 받아와지지 않는 줄 알았다. 하지만 그것은 아니었다! 그러면 우리는 백엔드 Controller에서 받아올 때,

	@PostMapping("/register")
    public String createForm(@RequestBody MemberForm form){
        Member member = new Member();
        member.setUsername(form.getUsername());
        member.setPassword(form.getPassword());
        System.out.println(member.getUsername());
        System.out.println(member.getPassword());
        memberService.join(member);

        return "login";
    }

이렇게 받는데, 해당 MemberForm 클래스가 정의되기를

package com.example.SpringAndReact.controller;

public class MemberForm {
    private String UserId;
    private String password;

    public String getUserId() {
        return UserId;
    }

    public void setUserId(String userId) {
        UserId = userId;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

이렇게 정의가 되어있으니 이름이 달라서 userId는 받아와질 턱이 없었다.
따라서 다음처럼 바꾸고, 해당 파일을 불러오는 모든 코드를 수정해서 체크를 해보면

package com.example.SpringAndReact.controller;

public class MemberForm {
    private String username;
    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

4. 결과

  • 로그인 성공 콘솔 창

  • Controller에서 받아온 아이디 비밀번호

  • 중복 가입 구현 체크

profile
기록하고, 공유합시다

1개의 댓글

comment-user-thumbnail
2023년 1월 24일

잘 보고 갑니다!

답글 달기