Jun4928 / wanted-pre-onboarding-challenge-BE-task-JAN.2023

JAN.2023 wanted 프리온보딩 챌린지 BE 사전과제
29 stars 36 forks source link

사전과제 제출 #36

Open hsjkdss228 opened 1 year ago

hsjkdss228 commented 1 year ago

1. 본인이 작성했던 코드 중 공유하고 싶은 코드를 이유와 함께 마크다운 code block 을 사용해 올려주세요

public RegisterGameResultDto joinGame(Long gameId,
                                      Long currentUserId) {
    User currentUser = userRepository.findById(currentUserId)
        .orElseThrow(() -> new UserNotFound(currentUserId));

    Game game = gameRepository.findById(gameId)
        .orElseThrow(() -> new GameNotFound(gameId));

    List<Register> registers = registerRepository.findByGameId(gameId);

    Register register = game.join(currentUser, registers);

    if (register != null) {
        Register savedRegister = registerRepository.save(register);

        Post post = postRepository.findById(game.postId())
            .orElseThrow(PostNotFound::new);

        User postAuthor = userRepository.findById(post.userId())
            .orElseThrow(() -> new UserNotFound(post.userId()));

        Notice notice = savedRegister.createRegisterNotice(
            currentUser,
            postAuthor
        );

        noticeRepository.save(notice);
    }

    return new RegisterGameResultDto(gameId);
}

위 소스코드는 개인 프로젝트에서 Spring Boot로 구현한 서버 애플리케이션의 Application Layer에 있는 Service 로직의 하나를 가져온 것입니다. 코드 중 Register register = game.join(currentUser, registers)은 본래 다음의 구조였습니다.

registers.forEach(register -> {
    if (register.userId().equals(accessedUserId)
        && (register.status().value().equals(RegisterStatus.PROCESSING)
        || register.status().value().equals(RegisterStatus.ACCEPTED))) {
        throw new RegisterGameFailed("이미 신청 중이거나 신청이 완료된 운동입니다.");
    }
});
List<Register> members = registers.stream()
    .filter(register -> register.status().value().equals(RegisterStatus.ACCEPTED))
    .toList();

if (members.size() >= game.targetMemberCount().value()) {
    throw new RegisterGameFailed("참가 정원이 모두 차 참가를 신청할 수 없습니다.", game.id());
}

Register register = new Register(
    accessedUserId,
    gameId,
    new RegisterStatus(RegisterStatus.PROCESSING)
);

리팩터링 이전의 구조는 Application Layer에 비즈니스 로직이 드러나 있는 형태였고, 테스트 코드로 코드의 동작을 검증하면서 구현을 진행하는 과정에서 테스트를 위한 데이터를 세팅하는 로직이 너무 복잡해져 테스트 코드를 작성하기 어려웠습니다.

다른 분들의 도움을 받아 비즈니스 로직을 객체가 수행하도록 리팩터링을 진행하면서 코드가 간결해지고, 테스트 코드의 크기가 줄어드는 것을 확인할 수 있었습니다. 이전까지는 단순히 Layered Architecture가 유연한 소스코드를 작성하기 쉽다고만 들었고 체감이 잘 되지 않았지만, 좋지 못했던 구조의 코드를 리팩터링하면서 Layer의 역할에 따른 관심사의 분리의 필요성을 체감하는 계기가 되었기에 해당 소스코드를 공유하게 되었습니다.

Reference

2. Layered Architecture(계층 아키텍처)에 대해서 설명해 주세요

Layered Architecture는 소프트웨어 아키텍처의 한 종류로, 소프트웨어를 구성하는 conceptual한 요소들을 논리적으로 구조화하는 매커니즘인 Layer로 소프트웨어의 주요 동작인 Presentation, Application Processing, Data management를 논리적으로 분리합니다. Layer는 System infrastructure를 구성하는 하드웨어 요소들을 물리적으로 구조화하는 매커니즘인 Tier와 비교될 수 있습니다.

일반적인 객체지향 설계 환경에서 Layer는 다음의 4가지 계층으로 구분됩니다.

Layered Architecture의 가치는 관심사를 분리해 각 Layer에서 특정 영역을 집중적으로 다루게 하는 데 있습니다. 각각의 Layer에 관련된 코드를 Layer 별로 집중시킴으로써 로직이 분리되어 있지 않은 경우 대비 각각의 Layer를 훨씬 명료하고, 유연하고, 재사용 가능한 구조로 설계하고 구축할 수 있습니다.

Layer 간의 연결은 설계 의존성을 한 방향으로 두어 느슨하게 결합하도록 합니다. 일반적으로 상위 Layer이 하위 Layer의 인터페이스를 호출하는 식으로 이루어지고, 경우에 따라 하위 수준의 Layer가 상위 수준의 Layer와 통신해야 할 경우에는 콜백이나 관찰자 패턴을 적용하기도 합니다.

References

3. Dependency Injection(의존성 주입)의 개념과 함께, 왜 필요한지 작성해 주세요

Dependency Injection은 디자인 패턴의 하나로, 객체 또는 함수가, 의존하고 있는 다른 객체 또는 함수를 전달받아 사용하는 패턴입니다.

Java의 웹 프레임워크인 Spring Boot에서 MVC 패턴을 적용하는 경우, 하위 Layer를 상위 Layer에 다음과 같은 방식으로 의존성을 주입할 수 있습니다.

// controllers/PlaceController.java
@RestController
@RequestMapping("places")
public class PlaceController {
    private final GetPlaceService getPlaceService;

    public PlaceController(GetPlaceService getPlaceService) {
        this.getPlaceService = getPlaceService;
    }

    @GetMapping("{placeId}")
    public PlaceDto place(
        @PathVariable Long placeId
    ) {
        return getPlaceService.getTargetPlace(placeId);
    }
}

// services/GetPlaceService.java
@Service
@Transactional
public class GetPlaceService {
    // ...
}

Dependency Injection은 다음과 같은 이점을 가집니다.

Reference

4. 본인이 사용하는 언어의 Functional Programming(함수형 프로그래밍) 스펙을 예제와 함께 소개해 주세요

Java는 Java 8부터 함수형 프로그래밍을 위한 인터페이스로 람다식, Stream API, 함수형 인터페이스를 제공하고 있습니다.

1. 람다식

Java의 람다식은 익명 함수를 구현하는 방식 중의 하나로, 다음과 같이 구성됩니다.

FunctionalInterface example = () -> System.out.println("message");

람다식을 구성하는 요소는 다음과 같습니다.

2. Stream API

Stream API는 컬렉션이나 배열에 저장된 요소들에 순차적으로 혹은 병렬적으로 접근해 특정한 동작을 처리하는 메서드들을 지원하는 컬렉션입니다.

Stream API를 이용해 특정 컬렉션의 모든 요소를 화면에 출력하는 코드의 예시는 다음과 같습니다.

List<String> messages = List.of("Hello", "World", "How", "Are", "You");

messages.stream()
    .sorted()
    .forEach(System.out::println);
Are
Hello
How
World
You

3. 함수형 인터페이스

Java에서 함수형 인터페이스는 1개의 추상 메서드를 갖는 interface를 의미합니다. 함수형 인터페이스는 람다식을 이용해 구현체를 생성할 수 있습니다.

Java에서 제공하는 함수형 인터페이스에는 Runner, Supplier<T>, Consumer<T>, Function<T, R>, Predicate<T> 등이 있습니다.

Runnable runnable = () -> System.out.println("message");
runnable.run();
Supplier<String> supplier = () -> "message";
System.out.println(supplier.get());
Consumer<String> consumer = (message) -> System.out.println(message);
consumer.accept("message");

cf. 람다식이 하나의 인자를 받아 해당 인자를 특정 메서드의 인자로 넘겨주는 동작을 수행할 경우, method reference의 형태로 나타낼 수 있습니다.

// 위의 Consumer와 동일한 결과를 반환합니다.
Consumer<String> consumer = System.out::println;
consumer.accept("message");
Function<String, Integer> function = String::length;
System.out.println(function.apply("Hello"));
Predicate<String, Integer> predicate = (string) -> string.startsWith("a");
System.out.println(predicate.test("abcdefg"));
References

5. (코드 작성) 다음 스펙을 만족하는 delay 함수를 작성해 주세요 (hint: Promise 사용)

type SomeFunctionReturnString = () => string

function delay(f: SomeFunctionReturnString, seconds: number): Promise<string> {
    // 해당 함수 내부를 구현해 주세요
};

const success = () => {
  return "successfully done";
};

const fail = () => {
  throw new Error("failed");
};

delay(success, 2)
  .then((res) => console.log(res))
  .catch((e) => console.log(e));

delay(fail, 2)
  .then((res) => console.log(res))
  .catch((e) => console.log(e));

결과값

  $ ts-node delay.ts
  successfully done
  Error: failed
function delay(f: SomeFunctionReturnString, seconds: number): Promise<string> {
  let message: string;

  const checkError = () => {
    try {
      message = f();
    } catch (error) {
      if (error instanceof Error) {
        const errorMessage = error.message
        setTimeout(() => console.log(`Error: ${errorMessage}`), seconds * 1000);
      }
    }
  }

  checkError();

  return new Promise<string>((resolve) => {
    if (message) {
      setTimeout(() => resolve(message), seconds * 1000);
    }
  });
};

6. 강의를 통해서 기대하는 바, 또는 얻고 싶은 팁을 적어주세요

백엔드 개발자로의 첫 취업을 준비하면서 Java와 Spring Boot를 학습하고, 개인 프로젝트를 진행하면서 학습한 것들을 적용해보는 것을 시도했습니다. 취업 준비를 위해 채용 공고를 둘러보면서 기술 스택으로 Java와 Spring Boot뿐만 아니라 JavaScript, TypeScript, node.js를 사용하고 있는 기업들도 많음을 확인할 수 있었습니다. Java뿐만 아니라 JavaScript, TypeScript를 이용해서도 백엔드 애플리케이션을 제작하는 역량을 기르고, 새로운 영역에 적극적으로 도전해 성취하는 경험을 하고 싶습니다.