예전에 입대하기 직전에 장고로 만들었던

todo 리스트?, 개발일지 작성,관리 앱?  개발 일정 관리 캘린더앱?

뭔지 모르겠는데 아무튼 그 프로젝트를 Spring으로 다시 만들어보기로 했다.

만들다 보니 재밌었던 부분, 새롭게 배운점, 고민점들이 많이 생겨서 여기에 적어보기로 했다.

 

사실 만들면서 정리해야 했던 글인데 귀찮아서 미루고 미루다가 대충 마무리 하고 적게 됐다.

이 글을 읽는 분들이 알고 있는 점이나 비슷한 경험, 좋은 아이디어가 있다면 댓글 달아주면 많은 도움이 될 것 같다.

 

1. 주요 기능 설계

  1. 유저 회원가입, 로그인 기능
  2. 유저들끼리 모여서 스터디를 구상할 수 있는 워크스페이스 기능
  3. 워크스페이스 내에서 일정을 추가하고 참여할 수 있는 기능
  4. 일정 내에서 본인의 코멘트(개발일지)를 작성할 수 있는 기능

 

2. 엔티티 설계

보다시피 유저와 워크스페이스, 유저와 스케줄은 N:M의 관계이다.

그리고 사진엔 안나와있지만 워크스페이스:스케줄은 1:N 관계이며

유저,스케줄과 개발일지 역시 1:N의 관계이다.

 

1) pk값으로 뭘 사용하는 것이 좋을까?

pk값을 뭘로 할지 고민을 많이 해봤다.

필드마다 unique 한 특성을 많이 가지는 컬럼이 많다.

User은 로그인시 사용하는 id인 accountId

Workspace는 이름인 name 등등..

 

결론은 name, accountId등은 나중에 바뀔 가능성이 존재하므로

pk는 그냥 increment PK인 id를 만든 후 사용하기로 했다.

 

나중에 DB를 변경하거나 분산할 일이 존재하면 UUID로 하는 방법도 있겠지만,

이번 프로젝트에선 그럴 일이 절대 없으므로 그냥 increment PK를 쓰기로 했다. 

 

2) 비밀번호를 DB에 그대로 저장해도 될까?

당연히 안된다.

이번 프로젝트는 토이 프로젝트라 귀찮아서 그냥 varchar을 사용하고 넘어갔는데,

좀 더 규모가 커지거나 실제로 배포할 프로젝트라면 당연히 문제가 될 것이다.

 

정확히는 DB가 유출될 경우 피해가 될 수 있는 개인정보(이름, 이메일, 비밀번호)등은 암호화해서 저장해야한다.

이 부분은 나중에 공부를 더 한 후 추가해봐야겠다.

 

 

3. api 명세 설계 - N vs M 관계에서 rest api를 설계하는 방법

1) 프론트 없이 api 설계하기?

예전에 만들었던 api 명세를 조금 수정하려 했는데, 생각해보니까 이 프로젝트 프론트랑 제대로 된 피그마가 없다.

그냥 api만 만들면 되니까, 상상속의 프론트와 피그마로 api를 만들려고 했는데 정말 잘못된 생각이었다.

사용자 화면에 맞춰서 api를 설계하는 것이 기본인데, 사용자 화면이 명확하게 정의되어 있지 않으니까

api 명세도 계속해서 바뀌고, 일관성도 없어졌다. 이번 프로젝트에서 내가 한 가장 큰 실수였던 것 같다...

 

2) N vs M (워크스페이스/유저, 스케줄/유저) 관계에서의 url 설계

특정 워크스페이스에 속한 유저를 모두 찾아야 할 때?

/workspaces/1/users

/users?workspaceId=1

 

반대로 특정 유저가 참여하는 워크스페이스를 모두 찾을 때?

/users/2/workspaces

/workspaces?userId=2

 

어떤 구조가 더 좋을까? 에 대해 많이 고민해봤다.

결론은 상황에 맞춰서 쓰자인 것 같다.

프론트에서 구현할 화면에 어떤 정보가 중심인지, 다른 부가 정보는 뭐가 필요한지를 잘 고려해서 선택해야 할 것 같다.

 

둘 다 구현해 놓는 방법도 있고,

workspaces에서 검색 조건이 다양하고 자주 쓰인다 싶으면 후자를

그렇지 않다면 전자를 사용할 것 같다.

혹은 /woskspace/1 같은 url에서 유저 정보를 포함한 상세 정보를 모두 보내는 방법도 있다.

 

물론 그러면 유저 정보 외에 다른 불필요한 정보가 보내지는 단점도 있는데, 이는 restAPI의 어쩔 수 없는 단점인 것 같다.

이부분은 graphQL을 좀 더 공부해봐야 될 것 같다.

 

3) url에 pk값을 그대로 사용해도 될까?

보통 pk값 필드로 auto increment 를 사용할텐데

이러면 사용자가 url에 pk값 앞 뒤로 다른 값을 넣어서 다른 사용자의 데의터를 조회할 수 있다.

그렇다고 api url에 pk 값 대신 다른 값을 넣기엔 마땅한 unique 값이 없어 새로운 필드를 정의해야하고 복잡해진다.

uuid를 pk에 사용하기엔 128bit 라는 uuid 용량 문제와 url이 복잡해진다는 문제가 있다.

 

결론은 그냥 pk값을 그대로 쓰면서 인증 인가를 사용해서 정보 접근에 제한을 두려고 한다.

 

 

4. 구현과정에서 발생한 고민점들

 

1) Dto 매핑전략

https://www.inflearn.com/questions/30618/%EA%B6%81%EA%B8%88%ED%95%A9%EB%8B%88%EB%8B%A4

 

궁금합니다. - 인프런 | 질문 & 답변

안녕하세요 진짜 좋은 강의와 선생님의 답변으로 많이 배우고 있습니다. 강의를 따라 하다보니 몇가지 궁금증이 생겼습니다.1. controller에서 responseEntity 를 안쓰시던데 딱히 이유가 있을까요?2.계

www.inflearn.com

API 를 제작하다 보면 Entity를 직접 반환하면 안되므로 필연적으로 Dto를 사용하게 된다.

View 단계에서 사용하는 Dto를 Controller, Service 중 어느 곳에서 변환하는 것이 좋을까 고민을 많이 해봤다.

 

i) Controller에서 변환하기

장점 : service가 dto에 의존하지 않게 된다. (의존관계 단순화)

dto는 화면 구현에 의존해 api 설계가 변경될 시 쉽게 바뀔 수 있음

dto 세부사항 수정시 service의 코드가 바뀌지 않아도 됨

 

단점 : Entity 생성시 의존관계가 있는 Entity 생성을 controller에서 맡아서 하게 되는데

핵심 비즈니스 로직 이라고 할 수 있는 중복 조회, 연관관계 매핑 등을 Controller 단에서 하게 됨.

이는 Controller가 여러 Service 객체들에 의존하게 만듦.

OSIV 문제도 발생. dto 매핑을 트랜잭션 종려 후 진행하면 Lazy Loding을 처리할 경우 오류가 발생함.

 

ii) Service에서 변환하기

장점 : service가 핵심 비즈니스 로직 (중복 조회, 연관관계 매핑) 등을 모두 담당하고 controller의 코드가 깔끔해짐.

dto 변환을 Service 함수 트랜잭션 내에서 처리하므로 osiv 문제 해결.

 

단점 : service 객체가 화면(dto)에 의존하게 됨.

dto 변경 시 service 코드도 변경해야함.

 

결론은 섞어서 사용하기로 했다.

이게 뭔가 싶겠지만 dto 변환을 controller에서 하기 힘든 가장 힘든 상황이 Entity 생성 시 로직 때문이었다.

그렇다고 조회 등의 모든 상황에서 dto가 service로 보내진다면 service 코드가 의존하는 dto가 너무 많아진다.

 

그러나 생성 시에는 결국 dto가 변경되면 controller, service 혹은 mapper를 사용하면 mapper객체든

연관관계가 있는 다른 객체를 같이 바꾸는 수 밖에 없는 것 같았다.

 

그래서 생성 로직만 service 객체까지 dto를 보내고, 나머지 다른 로직에선 controller에서 변환해서 service에 보내기로 했다.

그리고 나중에 프로젝트 규모가 커지면

그때 osiv를 false로 바꾸고 조회 생성 모두 dto에 의존하는 api 전용 service를 새로 만들고 분리시키는 것이 좋을 것 같다.

 

2) Dto 파일들 관리 방법

영한 센세의 답변이다.

i) 도메인 패키지 안에 포함하기

ii) 별도의 dto 패키지를 만들기 (여러 패키지에서 공유될 시)

iii) 사용되는 repository나 service 계층에 두기

 

나는 여러 패키지에서 공유하지 않는데도 2번을 사용했다.

이게 구조가 제일 깔끔하고 사용하기 편한 것 같다.

대신 dto에서 Entity를 제외한 다른 패키지를 절대 의존하지 않도록 했다.

 

3)Swagger를 꼭 써야할까?

 

그냥 postman 쓰면 안되는 걸까?

라는 생각을 하고 있었는데 프론트로 일하고 있는 친구가 제발 써달라고 했다.

프론트에서 요청과정에서 실수 방지, 결과 테스트가 훨씬 편하다고 했었던 것 같다.

 

springdoc을 이용해서 swagger를 구현하기로 했다.

https://colabear754.tistory.com/99

 

[Spring Boot] Springdoc 라이브러리를 통한 Swagger 적용

목차 기본 환경 IntelliJ Ultimate 2022.3 Spring Boot 2.7.7 Kotlin 1.7.21(JDK 11) Springdoc Openapi UI 1.6.11 Springdoc은 무엇인가? 이전에 Spring Boot 프로젝트에 Swagger UI를 적용하는 포스트를 작성한 적이 있다. 해당 포

colabear754.tistory.com

 

 

//MemberServiceImpl
@Component
public  class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;
    
    public MemberServiceImpl(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }
    
    public void join(Member member){
        memberRepository.save(member);
    }
    
    public Member findMember(Long id){
        return memberRepository.findById(id);
    }
}

//MemoryMemberRepository
@Component
public class MemoryMemberRepository implements MemberRepository{
    private static Map<Long, Member> store = new HashMap<> ();
    
    @Override
    public void save(Member member){
        store.put(member.getId(), member);
    }
    @Override
    public Member findById(Long id){
        return store.get(id);
    }
}

다음과 같이 컴포넌트 어노테이션을 이용해 스프링 빈에 자동으로 인스턴스를 등록할 수 있다.

문제는 MemberServiceImpl 객체는 MemberRepository 인터페이스와 의존관계를 가지고 있다.

 

수동 빈 등록에선 AppConfig 파일에서 직접 의존관계를 주입해줬는데 자동 등록에선 어떻게 의존관계를 설정할까??

 

@Autowired - 자동 의존관계 주입

//MemberServiceImpl
@Component
public  class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;
    
    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }
    
    public void join(Member member){
        memberRepository.save(member);
    }
    
    public Member findMember(Long id){
        return memberRepository.findById(id);
    }
}

@Autowired는 자동 의존관계 등록 어노테이션이다.

Autowired를 달아놓으면 스프링이 컴포넌트 스캔 후 빈을 등록할 때 자동으로 의존관계를 주입시켜준다.

 

스프링 빈이 컴포넌트 스캔을 할때 생성자에 Autowired 어노테이션이 붙은 인스턴스는

생성자의 파라미터를 보고 같은 타입의 빈을 자동으로 주입해준다.

 

중복 등록과 충돌

@Component
public class MemberA{}

@Component
public class MemberB {}


@Component
public class MemberService{
    private Member member;
    
    @Autowired
    public MemberService(Member member){
    	this.member = member;
    }
}

만약 다음과 같이 같은 클래스 타입의 빈이 두 개 이상이라면 어떻게 될까??

 

NoUniqueBeanDefinitionException발생한다.

 

다음과 같은 오류를 해결하는 방법은

 

1. 이름으로 매칭

public class MemberService{
    private Member memberA;
    
    @Autowired
    public MemberService(Member memberA){

다음과 같이 필드명을 memberA라고 명시해주거나

@Autowired("memberA")
public MemberService(Member member){

어노테이션안에 이름을 입력해준다면 스프링이 이름이 동일한 memberA를 자동으로 주입해준다.

 

2. Qualifier 사용하기

@Component
@Qualifier("A")
public class MemberA{}

@Component
public class MemberB {}


@Component
public class MemberService{
    private Member member;
    
    @Autowired
    public MemberService(Member @Qualifier("A")member){
    	this.member = member;
    }
}

빈 이름이 아닌 추가 구분자를 제공하는 기능이다.

 

3. Primary 사용하기

@Component
@Primary
public class MemberA{}

@Component
public class MemberB {}


@Component
public class MemberService{
    private Member member;
    
    @Autowired
    public MemberService(Member member){
    	this.member = member;
    }
}

빈 등록시 우선순위를 지정해주는 방법이다.

여러개의 빈이 존재할 때 @Primary가 붙어있는 빈을 먼저 주입해준다.

 

 여러개의 빈을 조회해야 할때 - List, Map

예를 들면 방금과 같은 코드에서 서비스가 실행되기 전까지는

MemberA,MemberB 둘 중 어떤 인스턴스 빈을 사용할 지 모르는 상황이 생길 수 있다.

 

@Component
public class MemberA{}

@Component
public class MemberB {}


@Component
public class MemberService{
    private Map<String,Member> memberMap;
    private List<Member> memberList
    
    @Autowired
    public MemberService(Map<String,Member> memberMap, List<Member> memberList ){
    	this.memberMap = memberMap;
        this.memberList = memberList;
    }
}

그럴땐 Map,List를 이용해 여러개의 빈을 받아온 후

사용시점에 동적으로 선택해 사용할 수 있다.

 

자동, 수동의 올바른 실무 운영 기준

  1. 편리한 자동 등록을 주로 사용하자
    1. 최근 스프링 부트는 컴포넌트 스캔을 기본으로 지원함
    2. 관리할 빈이 많아지면 수동 등록은 번거로움
    3. 자동 등록으로도 DIP,OCP 지킬 수 있음
  2. 수동은 언제 쓸까?
    1. 업무 로직 빈
      1. 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터를 처리하는 리포지토리등이 업무 로직
      2. 비즈니스 요구사항을 개발할 때 추가하거나 수정됨
      3. 숫자가 매우 많음, 보통 문제가 발생하면 찾기 쉬움 → 자동 등록
    2. 기술 지원 빈
      1. 기술적인 문제나 AOP(공통 관심사)를 처리할때 주로 사용됨
      2. 데이터 베이스 연결, 공통 로직 처리
      3. 수가 적음, 그러나 광범위한 영향, 어디서 문제인지 찾기 힘듦 → 수동 등록
      4. 수동 등록 후 설정 정보에 바로 나오도록 하는게 유지보수 쉬움

 

 

 

지난 번글에서 스프링 빈과 스프링 컨테이너를 사용하는 이유, 작동원리에 대해 썼다.

이번 글에선 스프링 빈을 어떤식으로 등록하고 사용하는지 조금 더 자세하게 써보려고 한다.

 

인스턴스를 스프링 빈으로 등록하는 방법

1. 수동 등록

 

원하는 객체 인스턴스를 하나씩 직접 스프링 빈으로 등록하는 방법.

Xml, 지난 번에 사용했던 Configuration 어노테이션을 사용한 방법 등이 있다.

지난번 글에서 Xml 을 사용하는 방법은 최근에는 거의 쓰이지 않고 있으므로 Configuration 을 사용하는 방법만 간단하게 짚고 넘어갔다.

//AppConfig
@Configuration
public class AppConfig{
    // 설정 내용
    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
    .
    .

Config 클래스를 만들고 Configuration 어노테이션을 붙인 다음 Bean 어노테이션과 메소드를 통해 빈을 등록하면 된다.

메소드 이름으로 빈 이름을 정할 수 있고 의존관계 또한 메소드를 그대로 사용하면 된다.

 

단점 : 빈을 개발자가 일일히 등록해야함 -> 규모가 커지면 누락 및 오류 발생 가능성 커짐

 

2. 자동 등록

 

수동 등록의 단점을 해결하기 위해 스프링에서 제공하는 기능이다.

//AutoAppConfig
package project;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import static org.springframework.context.annotation.ComponentScan.*;

@Configuration
@ComponentScan
public class AutoAppConfig {
}

설정 파일 앞에 ComponentScan 어노테이션을 붙여주기만 하면 된다.

AutoAppConfig라는 클래스를 만들고 Configuration과 ComponentScan 어노테이션을 붙여줬다.

컴포넌트 스캔은 이름 그대로 Component 어노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다

//MemberServiceImpl
package project.member;
import org.springframework.stereotype.Component;

@Component
public  class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;
    
    public MemberServiceImpl(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }
    
    public void join(Member member){
        memberRepository.save(member);
    }
    
    public Member findMember(Long id){
        return memberRepository.findById(id);
    }
}
//MemoryMemberRepository
package project.member;

import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.HashMap;

@Component
public class MemoryMemberRepository implements MemberRepository{
    private static Map<Long, Member> store = new HashMap<> ();
    
    @Override
    public void save(Member member){
        store.put(member.getId(), member);
    }
    @Override
    public Member findById(Long id){
        return store.get(id);
    }
}

빈으로 등록하길 원하는 클래스 앞에 Component 어노테이션을 붙여주기만 하면 끝이다.

이러면 스프링이 자동으로 Component 어노테이션이 붙은 클래스들을 찾아서 빈으로 등록한다.

이때 기본 이름은 클래스 이름에서 맨 앞글자만 소문자로 바꾼 형태이며

Component("beanName") 과 같은 방식으로 직접 등록할 수도 있다.

 

자동 등록 시의 의존관계 설정은 @Autowired 어노테이션을 사용한다.

Autowired와 자동 의존관계 설정에 관해선 다음 글에서 좀 더 자세히 다룰 예정이다.

 

스캔 범위 지정하기

모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸린다.

그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.

//basePackages
//탐색할 패기지의 시작 위치 지정. 이 패키지를 포함한 하위 패키지를 모두 탐색
@ComponentScan(basePackages = "project")
//여러 시작 위치 지정도 가능
@ComponentScan(basePackages = {"project.core", "project.service"})

//basePackageClasses
//지정한 클래스의 패키지를 탐색 시작 위치로 지정한다
@ComponentScan(basePackagesClasses = MemberServiceImpl.Class)

만약 시작 위치를 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.

최근 스프링 부트의 기본 제공 방법은 패키지 위치를 지정하지 않고, 설정 정보 클래스 위치를 프로젝트 최상단에 두는 것이다.

 

필터

includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.

excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.

@ComponentScan(
	includeFilters = {
		@Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
	},
	excludeFilters = {
		@Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
	}
)

FilterType은 5가지 옵션이 존재한다.

ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.

ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.

ASPECTJ: AspectJ 패턴 사용

REGEX: 정규 표현식

CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리

 

 

중복 등록과 충돌

만약 컴포넌트 스캔에서 같은 빈 이름을 사용해 두 개 이상의 빈이 등록된다면 어떻게 될까?

1. 자동 빈 등록 방법에서만 같은 빈 이름이 두 개 생성됐을때 -> 스프링이 오류를 띄운다.

2. 수동으로 등록한 빈과 자동으로 등록한 빈의 이름이 겹칠때 -> 수동 등록한 빈이 우선권을 가진다.

그러나 최근에는 그냥 오류를 띄우도록 바뀌었다.

 

 

 

 

 

 

 

 

 

 

지난번 글에선 순수 JAVA 코드로 예재를 구성하고

IOC와 DI 그리고 DI 컨테이너에 대해 다뤘다.

 

이번 글에선 스프링을 이용해서 예재 코드를 조금 수정해보려고 한다.

 

AppConfig 코드를 다음과 같이 수정하자

//AppConfig
package project;

import project.member.MemberService;
import project.member.MemberServiceImpl;
import project.member.MemoryMemberRepository;
import project.member.MemberRepository;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig{
    // 설정 내용
    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
    .
    .

AppConfig 에 우선 Configuration 이라는 어노테이션을 달아줬고,

MemberService와 memberRepository 위에 Bean 이라는 어노테이션을 달아줬다.

 

그리고 MemberService를 사용하는 MemberApp 파일을 수정해주자

//MemberApp
package project;

import project.member.Member;
import project.member.MemberService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MemberApp {
	public static void main(String[] args) {
    	//AppConfig appConfig = new AppConfig();
        //MemberService memberService = appConfig.memberService();
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService",MemberService.class);
        .
        .

AppConfig에서 직접 memberService를 호출하는 것이 아니라

ApplicationContext 라는 클래스와 getBean이라는 메소드를 이용해서 memberService를 사용하고 있다.

 

무슨 차이점이 있는걸까?

스프링을 사용하면 무슨 장점이 존재할까?

 

스프링 빈과  스프링 컨테이너

코드에서 사용된 ApplicationContext를 스프링 컨테이너라고 부른다.

ApplicationContext 는 자바 인터페이스이다.

스프링 컨테이너는 XML을 기반으로 만들 수 있고, Configuration 어노테이션을 이용해서도 만들 수 있다.

지금 사용하는 방식이 어노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것이다.

 

스프링 컨테이너는 Configuration 어노테이션이 붙은 설정 클래스의 메소드를 분석한다.

그 후 설정 클래스 정보를 활용해 객체 인스턴스를 스프링 빈으로 등록해준다.

이때 빈 이름과 빈 객체로 나눠서 저장하게 된다.

빈 이름 빈 객체
memberService MemberServiceImpl@x01..
memberRepository MemoryMemberRepository@x02..

이때 빈 이름은 메소드 이름으로 자동 저장되며 @Bean(name="memberService2")와 같이 직접 지정할 수도 있다.

 

그렇다면 객체를 스프링 빈으로 지정하고, 컨테이너에서 관리하는 이유는 뭘까?

바로 싱글톤 패턴을 지원하기 위해서다.

 

싱글톤 패턴이란?

싱글톤 패턴은 클래스의 인스턴스가 딱 한개만 생성되는 것을 보장하는 패턴이다.

객체를 한번만 생성하고 그 이후에는 공유해서 사용하기 때문에 객체를 효율적으로 사용 가능하다.

 

예를 들면

기존의 AppConfig 클래스 코드에선 MemberApp 객체를 사용할때마다 새로운 MemberServiceImpl 객체를 생성해서 주입시켜준다.

3명의 사용자가 MemberApp을 사용한다면 3개의 객체를, 100명이면 100개를 만명이면 객체 만개를 생성하고 관리해야한다.

그러나 싱글톤 패턴을 이용한다면 하나의 객체만 생성한 후 사용자들에게 같은 객체를 전달하면 된다.

//참고
//싱글톤 패턴을 자바 코드로 구현하기
public class SingletonService {
	private static final SingletonService instance = new SingletonService();
	
	private SingletonService(){
	}
	public SingletonService getInstance(){
		return instance;
	}
}

 

스프링 컨테이너는 객체 인스턴스를 빈으로 저장해 싱글톤 패턴으로 관리해준다.

한 번 클래스를 빈으로 등록해놓으면 해당 클래스를 사용할 일이 생겼을때

getBean 메소드를 사용한다면 항상 같은 객체 인스턴스를 반환하도록 스프링이 알아서 관리해준다.

 

물론 싱글톤 패턴의 단점도 존재한다.

  1. 패턴을 구현하는데 시간이 오래걸림
  2. 의존관계상 클라이언트가 구현에 의존 -> DIP 위배, OCP위배 가능성 높음
  3. 테스트하기 어려움
  4. 자식 클래스 만들기 어려움

그러나 스프링을 활용하면 싱글톤 패턴의 문제점은 해결하며 객체 인스턴스를 싱글톤으로 관리할 수 있다.

그렇다면 스프링 컨테이너는 어떻게 동작하는걸까?

스프링 컨테이너의 동작 원리

void configurationDeep() {
	ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
	MemberService bean = ac.getBean(MemberService.class);
 
 System.out.println("bean = " + bean.getClass());
 //출력: bean = class project.member.MemberService$$EnhancerBySpringCGLIB$$bd479d70
}

만약 bean이 순수한 MemberService 클래스라면 다음과 같이 출력되야한다.

bean = class project.member.MemberService

그러나 실제 출력값은 $$와 함께 xxx..CGLIB가 붙어서 나오는 것을 볼 수 있다.

 

이는 스프링이 자바의 바이트 코드를 조작하여 싱글톤 패턴을 유지하기 때문이다.

스프링이 CGLIB라는 라이브러리 사용해 MemberService를 상속받은 다른 가짜 클래스를 만들고 빈으로 등록한 것이다.

이때 MemberService를 상속받았기 때문에 MemberService 클래스로도 호출이 가능하다.

 

그리고 MemberService를 호출한다면 가짜 클래스를 불러내는데 가짜 클래스에는 다음과 같은 메소드가 추가되어있다.

1. MemberService 가짜 객체가 이미 존재한다면 -> 존재하는 객체를 반환

2. 객체가 존재하지 않는다면 -> 새로 생성해서 반환

 

그래서 스프링은 다음과 같은 방식으로 싱글톤 패턴을 지원한다.

1. Configuration 어노테이션이 붙은 클래스 안의 메소드를 읽어낸다.

2. 메소드의 클래스를 상속받는 가짜 클래스 인스턴스를 만들어 빈으로 등록한다.

3. 위와 같이 가짜 클래스를 활용해서 싱글톤 패턴을 지원

 

싱글톤 방식을 사용할 때 주의할 점

싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.

 

무상태(stateless)로 설계해야 한다!

 

특정 클라이언트에 의존적인 필드가 있으면 안된다.

특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!

가급적 읽기만 가능해야 한다.

필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

 

스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!

 

라고 하는데 이건 나중에 공부를 더 해봐야 할 것 같다.

 

 

이번 글에선 스프링 빈이 무엇인지와 어떤 원리로 작동하는지를 써봤다.

다음 글에선 스프링 빈 활용법에 대해 써보려고 한다.

 

 

 

 

 

 

 

1. 스프링 사용 없이 구현해보기

강의에서 제공하는 회원 서비스와 다이어그램은 다음과 같다.

간략하게 회원 서비스만 다뤄보자

//MemberServiceImpl
package project.member;

public  class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    
    public void join(Member member){
        memberRepository.save(member);
    }
    
    public Member findMember(Long id){
        return memberRepository.findById(id);
    }
}
//MemoryMemberRepository
package project.member;

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

public class MemoryMemberRepository implements MemberRepository{
    private static Map<Long, Member> store = new HashMap<> ();
    
    @Override
    public void save(Member member){
        store.put(member.getId(), member);
    }
    @Override
    public Member findById(Long id){
        return store.get(id);
    }
}

MemberRepository 라는 인터페이스를 만들고 MemoryMemberRepository 라는 구현 클래스를 만들었다.

저번에 설명한 역할과 구현이라는 설계 방식을 잘 따르는 것처럼 보인다.

 

그러나 만약에 외부 DB에 연결을 한 후 Member 정보를 DB에 저장하기로 결정했다면?

DbMemberRepository를 만든 후 MemoryMemberRepository와 교체하면 된다.

이는 OCP 원칙을 준수하는 것 처럼 보이지만 다른 문제점이 생긴다.

public  class MemberServiceImpl implements MemberService{
    //private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final MemberRepository memberRepository = new DbMemberRepository();
    .
    .
    .

바로 MemberServiceImpl 클래스를 변경해야지만 변경사항을 적용할 수 있다는 것이다.

Repository를 변경하기 위해 그걸 의존하는 클라이언트(Service) 클래스까지 변경해야 하고

이는 전체가 부분에 의존함을 의미한다. 즉 DIP, OCP 원칙을 위배한다고 볼 수 있다.

 

어떻게 해결 할 수 있을까??

 

관심사의 분리

어플리케이션을 공연, 인터페이스를 배역, 구현 객체를 배우라고 비유해보자.

지금 구현한 코드는 주연 배우가 본인의 원래 책임 뿐 아니라 조연 배우를 섭외하는 책임까지 가지고 있는 셈이다.

책임 즉 관심사의 분리가 필요하다.

 

공연 기획자의 역할을 하는, 즉 객체를 생성하고 의존관계를 연결하는 별도의 설정 클래스를 만들어보자

//MemberServiceImpl
package project.member;

public  class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;
    //생성자 주입
    public MemberServiceImpl(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}
    .
    .
}
//AppConfig
package project;

import project.member.MemberService;
import project.member.MemberServiceImpl;
import project.member.MemoryMemberRepository;
import project.member.MemberRepository;

public class AppConfig{
    // 설정 내용
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
}

우선 MemberServiceImpl 객체에 생성자를 만든 후 생성자의 매개변수를 통해 MemberRepository를 주입받도록 한다.

이것을 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라 한다.

이후 AppConfig라는 설정 클래스를 만든 후 AppConfig에서 객체들을 생성하면 된다.

 

이렇게 바꾼다면 MemverServiceImpl은 더 이상 MemoryMemberRepository에 의존하지 않게 된다.

단지 MemberRepository 인터페이스에만 의존하면 된다.

즉 구현이 아닌 역할에 의존하게 된다.

 

//MemberApp
package project;

import project.member.Member;
import project.member.MemberService;

public class MemberApp {
	public static void main(String[] args) {
		AppConfig appConfig = new AppConfig();
		MemberService memberService = appConfig.memberService();
        .
        .
        .

나중에 MemberService를 사용할 일이 생긴다면 다음과 같이 설정 파일에서 memberService를 불러오면 된다.

이렇게 구현한다면 DbMemberRepository로 교체할때도 오직 설정파일만

return new MemoryMemberRepository(); -> return new DbMemberRepository();

으로 교체하면 된다.

 

이렇게 변경한 코드는 스프링의 핵심 원리인 IOC와 DI를 사용하고 있다.

 

1. IOC(Inversion of Control , 제어의 역전)

 

기존 프로그램은 클라이언트 구현 객체가 스스로 서버 구현 객체를 생성하고 관리했다.

즉 구현 객체가 제어 프름을 스스로 조종했다.

AppConfig를 만든 후에 구현 객체는 자신의 로직만을 담담한다. 즉 프로그램의 제어 흐름을 AppConfig가 가져간다.

MemberServiceImpl 입장에서 제어 흐름의 권한은 모두 AppConfig가 관리하고 있다.

이처럼 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라
한다.

 

2. DI(Dependency Injection, 의존관계 주입)

 

 MemberServiceImpl은 MemberRepository 라는 인터페이스에 의존한다.

즉 프로그램이 실행되기 전까지 어떤 구현 객체가 사용될지 알 수 없다.

애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서
클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.

의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를
쉽게 변경할 수 있다.

 

AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을
IoC 컨테이너 또는 DI 컨테이너라 한다.
의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다.

 

 

하지만 몇가지 찜찜한 점이 존재한다.

1. 결국 Repository를 변경한다면 AppConfig의 코드를 바꿔야한다

2. 서비스가 커진다면 설정파일에서 하나하나 등록하고 관리하기 힘들지 않을까?

 

이런 문제점을 Spring이 제공하는 기능들을 통해 해결해보자.

 

 

 

 

 

스프링이란? 

자바 플랫폼을 위한 오픈 소스 애플리케이션 프레임워크

동적인 웹 사이트를 개발하기 위한 여러 가지 서비스를 제공하고 있다.

 

스프링 부트는 뭔가요?

스프링을 편리하게 사용할 수 있도록 지원해주는 프레임워크, 최근에는 기본으로 사용하는 추세

 

스프링을 왜 만들었나요?

자바 언어의 가장 큰 특징인 객체지향을 활용하기 위해

스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크

 

객체지향 프로그래밍이란?

컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것이다.

객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다

 

유연하고 변경에 용이하다는 점이 핵심

- 프로그램이 유연하다는 것이 뭘까?

->프로그램을 변경할 일이 생겼을 때 마치 부품을 갈아끼우듯이, 레고 블럭을 조립하듯이 쉽고 유연하게 변경하면서 개발할 수 있는 프로그램

 

 

객체지향 프로그래밍의 4가지 특징

 

1. 캡슐화

서로 연관있는 속성과 기능들을 하나의 캡슐(capsule)로 만들어 데이터를 외부로부터 보호하는 것

외부에서 객체가 어떻게 동작하는지 모르게 하는 것

(마치 자동차 운전자가 자동차가 엔진이 어떻게 움직이는지 알 필요가 없듯이)

이는 객체를 자율적으로 만들고 객체의 독립성, 책임 영역을 지킨다.

접근 제어자(private, public) 등을 활용해 구현한다

 

2. 상속

현실의 상속과는 많이 다름

기존의 클래스를 재활용하여 새로운 클래스를 작성하는 문법 요소

아버지-아들 같은 관계를 이야기하는 것이 아님

동물 - 펭귄 같은 사이의 관계에서 사용한다.

이를 is a 관계라고도 한다. (펭귄 is a 동물 -> 말이 된다면 상속을 올바르게 사용한 것)

객체지향의 사실과 오해 책에선 is a kind of가 좀 더 적절하다고도 이야기한다 (펭귄 is a kind of 동물)

 

3. 추상화

“사물이나 표상을 어떤 성질, 공통성, 본질에 착안하여 그것을 추출하여 파악하는 것”

-> 필요한 부분만 모아서 추출해내는 것을 말한다. ex) 지하철 노선도

객체의 공통적이고 필요한 부분만 모아서 추출하는 것을 이야기함

자동차, 오토바이 모두 사람을 태울 수 있고, 이동이 가능함 -> 두 객체 모두 Vichle(탈 것) 으로 추상화 가능하다.

자바의 interface 또는 추상 클래스(abstract class)로 구현 가능하다.

 

interface와 상속의 차이?

interface는 껍데기만 놔두고 구현 강요

상속은 구현해놓은 것 재사용 가능

  • 상위 클래스의 물려줄 특성이 많을수록 좋다
  • 인터 페이스는 구현을 강제할 메서드가 적을수록 좋다

 

4. 다형성

역할과 구현으로 세상을 구분하자 (로미오 - 배우)

이렇게 한다면 세상이 단순해짐 ->  유연, 변경에 용이해짐

클라이언트가 대상의 역할만 알면 됨 (마치 배우가 바뀌어도 로미오 역할만 할 줄 알면 상대역은 영향이 없다) 

클라이언트는 대상이 변경되어도 영향 없음

자바에선 오버라이딩, 인터페이스, 상속 등으로 구현한다.

 

메소드 오버라이딩과 오버로딩의 차이는 뭘까?

- 오버라이딩 - 부모 클래스나 상속받은 인터페이스의 메소드를 새로 재정의해 덮어씌우는 것

- 오버로딩 - 같은 이름의 메소드를 두 개 이상 정의하는 것 (매개변수의 타입이나 개수를 다르게 하여)

 

 

스프링 강의에선 이 중 가장 중요한것이 다형성이라고 이야기한다.

 

강의에선 객체를 설계할때

역할 - 인터페이스 

구현 - 인터페이스를 구현한 클래스,객체

방식으로 설계하라고 한다.

즉 역할을 먼저 부여한 후, 그 역할을 수행하는 구현 객체를 만들라고 이야기한다.

 

다형성의 본질 : 클라이언트를 변경하지 않고 기능을 유연하게 변경할 수 있도록 하는 것

이를 위해선 인터페이스를 안정적으로 설계하는 것이 정말 중요하다.

스프링은 다형성을 극대화해서 이용하도록 도와줌

 

좋은 객체지향 설계의 5원칙 - SOLID

  1. SRP (Single responsibility principle) - 단일 책임 원칙
    1. 한 클래스는 하나의 책임만 가져야한다.
    2. 변경이 있을때 파급이 적으면 단일 책임 원칙을 잘 따른 것
  2. OCP (Open/closed principle) - 개방 폐쇄 원칙
    1. 확장에는 열려있으나 변경에는 닫혀있어야함
    2. 새로운 기능 구현 -> 새 코드 생성 YES, 기존 코드 변경은 NO(인터페이스 구현한 새로운 클래스를 제작하자)
  3. LSP (Liskov substitution priciple) - 리스코프 치환 원칙
    1. 부모 클래스는 자식클래스로 대체 가능해야함
    2. 하위 클래스는 인터페이스 규약을 전부 지켜야함
    3. ex) 자동차 클래스의 엑셀 메소드 → 앞으로가라, 자식 클래스가 엑셀 밟으면 뒤로 간다면 → 원칙 위배
  4. ISP (Interface segregation principle) - 인터페이스 분리 원칙
    1. 특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스보다 좋음
    2. 사용자 클라이언트 → 운전자 클라이언트, 정비사 클라이언트로 분리
    3. 분리하면 정비 인터페이스 자체가 변경되어도 영향 없음
  5. DIP (dependency inversion priciple) - 의존성 역전 원칙
    1. 추상화에 의존해야지 구체화에 의존하면 안된다
    2. 클라이언트는 인터페이스만 바라봐야한다
    3. 역할에 의존해야한다. (구현에 의존하면 안됨)

 

그래서 이 객체지향의 원리가 스프링에선 어떻게 구현될까?

를 다음편에 써보려고 한다.

 

 

 

 

 

 

 

인프런 김영한 님의 "스프링 입문 - 코드로 배우는 스프링부트, 웹 MVC, DB 접근" 기술 강의를 듣고 정리해보려 합니다.

 

개발환경 - GOORM IDE

 

1. 구름 IDE에서 컨테이너 생성

 

구름 IDE 접속 -> 새 컨테이너 생성 -> 소프트웨어 스택 ->

Spring Boot 선택 -> Template Gradle로 설정 후 컨테이너를 생성한다.

강의에선 h2 데이터베이스를 사용한 예재를 진행하는데 h2 데이터베이스를 우분투 환경에 설치하기 귀찮다면

mysql 설치 항목에 체크한 후 mysql로 바꿔서 진행해도 된다.

2. JAVA 버전 & Gradle 업그레이드 

강의가 2020년에 처음 나온 강의라서

spring boot - 2.3.1

JAVA - 11

버전을 사용하고 있다.

 

그런데 구름 ide가 지원하는 spring boot 컨테이너는 놀랍게도 자바 8과 spring boot 2.0.1 을 기본 설정해줘서 업그레이드를 해줘야한다.

# JAVA 11 설치
# apt 업데이트
$ sudo apt-get update && sudo apt-get upgrade

#자바 다운로드
$ sudo apt-get install openjdk-11-jdk

#vi 편집기 사용
$ sudo vi /etc/environment

# 아래 내용 입력 후 저장
$JAVA_HOME="/usr/lib/jvm/java-11-openjdk-arm64"

# 자바 11 버전에 맞는 번호 입력 후 엔터
$ sudo update-alternative --config java

 

https://velog.io/@phjppo0918/%EC%8B%9C%EA%B0%84%ED%91%9C-%EC%9E%90%EB%8F%99%EC%83%9D%EC%84%B1-%EC%84%9C%EB%B9%84%EC%8A%A4-1.-goorm-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%83%9D%EC%84%B1-%EB%B0%8F-Gradle-%EC%84%A4%EC%B9%98

 

[시간표 자동생성 서비스] 1. goorm 컨테이너 생성 및 Gradle 설치

이게 안되네;; 구름 댕같다 ㄹㅇ참조 링크: https://jjeongil.tistory.com/1402nano창이 뜨면 아래 두 줄을 복사헤서 gradle.sh에 복붙하면 된다.

velog.io

#Gradle 업데이트

#nano 설치
$ sudo apt-get install nano

# gradle 6.5.1 설치
$ wget https://services.gradle.org/distributions/gradle-6.5.1-bin.zip -P /tmp
$ sudo unzip -d /opt/gradle /tmp/gradle-6.5.1-bin.zip

# 환경설정 파일 열기
$ sudo nano /etc/profile.d/gradle.sh

# 아래 두 줄 복사해서 gradle.sh에 입력하고 저장
export GRADLE_HOME=/opt/gradle/gradle-7.1.1
export PATH=${GRADLE_HOME}/bin:${PATH}

# 스크립트 실행 및 환경변수 로드
$ sudo chmod +x /etc/profile.d/gradle.sh
$ source /etc/profile.d/gradle.sh

spring 2.3.x 버전에 맞게 gradle 을 업그레이드 해주자

아래 두 줄은 Goorm ide 컨테이너를 실행할때마다 반복적으로 실행해줘야하는 것 같다.

 

3. build.gradle 파일 변경

build.gradle 파일이 스프링 2.0.1 버전에 맞게 생성되어있을텐데 바꿔줘야한다.

// build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '2.3.1.RELEASE'
	id 'io.spring.dependency-management' version '1.0.9.RELEASE'
}
 
group = 'io.goorm'
version = '0.0.1'
sourceCompatibility = '11'
 
repositories {
	mavenCentral()
}
 
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.assertj:assertj-core:3.11.1'
    testImplementation ('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    
    // JPA 및 DB 연결을 위한 라이브러리
    //implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    //implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    //runtimeOnly 'mysql:mysql-connector-java'
    //compileOnly 'org.projectlombok:lombok'
    //annotationProcessor 'org.projectlombok:lombok'
}

test {
    useJUnitPlatform()
}

spring-boot-starter-web : 웹서버(tomcat)과 웹 mvc 기능을 포함한다.

spring-boot-stater-thymleaf : 타임리프 엔진(html 파일 뷰 엔진)

implementation 'org.assertj:assertj-core:3.11.1 : 강의에는 없는데 test 파일 작성중 assertThat을 호출했더니 오류가 생겨서 추가한 라이브러리.

spring-boot-stater-test : 테스트 파일 작성용

org.springframework.boot:spring-boot-devtools : html파일 수정시 실시간 반영을 위한 라이브러리 .. 라고 하는데 작동을 안한다.

 

4. application.properties 수정 및 url과 포트 설정

server.port=${PORT:8080}

# 이 설정을 추가 안해주면 나중에 빈 추가 및 오버라이딩 관련 오류가 발생했다.
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

이후 상단의 프로젝트 -> 실행 url과 포트 에서

실행용 url이 있는지  연결 포트가 8080으로 설정 되어있는지 확인하고 없으면 새로 생성한다.

 

5. Spring 서버 시작해보기

$ gradle build
$ cd build/libs
$ java -jar spring_boot-0.0.1.jar

서버가 잘 열리면 성공이고 잘 안되면

 

1. gradle --version , java --version 을 통해 제대로 설치되었는지 확인해보자.

2. build.gradle 파일이 제대로 수정됐는지 확인해보자.

 

일기장 앱을 만드는 프로젝트를 예시로 들어보려고 한다.

 

프로젝트 구조는 다음과 같다

 

 

main.py : uvicorn 서버 실행 메소드가 담겨있는 메인 파일 , python3 app/main.py 로 실행하면 서버가 실행된다.

 

server 폴더

routes 폴더 : 라우터 파일들이 담겨있다. django의 urls.py 와 views.py 같은 느낌이라고 생각하면 될 것 같다

app.py : main.py 실행시 실행된다. FastAPI 앱 인스턴스 생성과 라우터 연결 등의 역할을 맡고 있다.

database.py : database와 연결된 부분을 관리한다. db에 정보를 저장하거나 불러오는 함수들을 가지고 있다.

exceptions.py : 예외처리 클래스들이 구현되어있다.

models.py : model 들이 구현되어있다. 응답양식, 데이터 모델 등등

 

자세한 코드를 알아보자

#main.py

import uvicorn

if __name__ == "__main__":
    uvicorn.run("server.app:app",host = "0.0.0.0", port = 8000, reload = True)

main.py 실행시 uvicorn을 이용해 app.py를 실행하는 서버를 띄운다.

 

#app.py

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from .routes import diary_router,search_router
from app.server.exceptions import APIException

app = FastAPI()
app.include_router(diary_router.router)
app.include_router(search_router.router)

@app.exception_handler(APIException)
async def api_exception_handler(request: Request, exc: APIException):
    return JSONResponse(
        status_code =  exc.status_code,
        content = exc.content
    )

app.py에선 FastAPI객체를 만들고 router를 추가한다.

예외처리 세팅도 여기서 한다.

 

#diary_router.py

from fastapi import APIRouter
from fastapi.encoders import jsonable_encoder
from app.server import database
from app.server.models import responseModel,Diary,UpdateDiary

router = APIRouter(prefix = "/diaries")

@router.get("",response_description="get all diaries")
async def get_all_diaries():
    response_message = "all diaries"
    diaries = await database.get_diaries()
    return responseModel(response_message, diaries)

@router.get("/{date}", response_description="read a diary")
async def get_a_diary(date: str):
    response_message = "read a diary"
    diary = await database.get_diary(date)
    return responseModel(response_message,diary)

/diaries 로 시작하는 url이 들어왔을때 app.py 에서 diary_router로 연결을 해준다.

diary_router에선 database.py의 db조회 함수를 사용해 db에서 일기장 정보들을 조회해 받아온다.

받아온 정보를 responseModel을 이용해 client에 반환한다.

 

#database.py

from motor import motor_asyncio
from .models import Diary
from app.server.exceptions import *

host = 'localhost'
port = 27017

#database settings
def get_db():
    client = motor_asyncio.AsyncIOMotorClient(host,port)
    db = client.iaryda
    return db

diary_collection = get_db().get_collection("diary")

#helpers
def diary_helper(diary) -> dict:
    diary.pop("_id")
    return diary

#db 조회, 관리 함수
async def get_diaries() -> list:
    result_data = []
    
    async for diary in diary_collection.find():
        result_data.append(diary_helper(diary))
        
    return result_data


async def get_diary(date: str) -> dict:
    diary = await diary_collection.find_one({"date" : date})
    if diary != None:
        return diary_helper(diary)
    else:
        raise DiaryDoseNotExistExecption()

database.py파일에서 db연결 및 조회 함수를 만들었다.

motor 를 이용해 몽고디비와 파이썬을 연결했다.

 

몽고디비에서 document를 저장할때 _id 항목을 생성해주는데

_id 항목은 굳이 client에 전달할 필요가 없으므로 _id를 제외해주는 diary_helper 함수를 만들어 사용했다.

 

오류가 발생하는 경우 exception 클래스를 이용해 오류 메세지를 전달한다.

 

#models.py

def responseModel(message, data) -> dict:
    if type(data) == list:
        return {
            "status" : 200,
            "message" : message,
            "data" : data
        }
    else:
        return {
            "status" : 200,
            "message" : message,
            "data" : [data]
        }

responseModel 함수는 models.py에 구현돼있다.

일정한 양식으로 응답을 할 수 있도록 해준다.

 

#exceptions.py

class StatusCode:
    HTTP_500 = 500
    HTTP_200 = 200
    
def make_error_content(message) -> dict:
    return {
        "status" : StatusCode.HTTP_500,
        "message" : message,
        "data" : []
    }
    
class APIException(Exception):
    status_code : int
    content: dict
        
class DiaryDoseNotExistExecption(APIException):
    def __init__(self,):
        self.status_code = StatusCode.HTTP_500
        self.content = make_error_content("Unvalid date. There is no diary in this date")

exceptions.py에선 오류 메세지 전달을 구현했다.

Exception객체를 상속하는 APIException 클래스를 먼저 만든 후 app.py에 exception_handler를 설정해준다.

 

그 이후 APIException 을 상속하는 exception 클래스들을 만든 후 사용하면 된다.

 

 

결론은 사용자에게서 요청이 오면 

app.py -> router -> database -> router ->  응답 반환 순으로 이루어진다.

 

router 파일들을 좀 더 손볼 필요가 있을 것 같긴한데

일단 이렇게 만들어놨다.

'FastAPI' 카테고리의 다른 글

사지방에서 FastAPI 공부하기 #1 - 개발환경 설정  (0) 2023.03.19

+ Recent posts