나는 개발하며 MVC 패턴을 잘 지키고 있었을까?

lango·2023년 5월 23일
1
post-thumbnail

들어가며

이전 글에서 MVC 패턴에 대해서 학습하고 고민해보았다. 이번에는 이전까지 개발했던 프로젝트들의 코드를 보며 MVC 패턴을 잘 지켜서 개발했는지 살펴보려 한다.

필자가 개발한 프로젝트는 주력 언어로 Java, 프레임워크로는 Spring Boot를 활용하였다. 이로 인해 당연하게도 글의 내용이 전반적으로 Spring Boot에서 MVC 패턴을 다루는 데 집중되어 있음을 알린다.


MVC 패턴 규칙들을 잘 지키며 개발했을까?

실제 개발을 진행할 때 MVC 패턴의 규칙을 잘 따르기 위한 방법들을 이전 포스팅에서 알아보고 배웠다. 그 규칙들은 다음과 같았다.

1. 모델은 컨트롤러나 뷰에 의존하면 안된다. (모델 내부에 컨트롤러 및 뷰와 관련된 코드가 있으면 안된다.)

2. 뷰는 모델에만 의존해야 하고, 컨트롤러에는 의존하면 안된다. (뷰 내부에 모델의 코드만 있을 수 있고, 컨트롤러의 코드가 있으면 안된다.)

3. 뷰가 모델로부터 데이터를 받을 때는 사용자마다 다르게 보여주어야 하는 데이터에 한해서만 받아야 한다.

4. 컨트롤러는 모델과 뷰에 의존해도 된다. (컨트롤러 내부에는 모델과 뷰의 코드가 있을 수 있다.)

5. 뷰가 모델로부터 데이터를 받을 때는 반드시 컨트롤러에서 받아야 한다.

위 MVC 패턴의 규칙들을 기반으로 필자가 개발했던 프로젝트들의 코드를 보며 리뷰를 시작해보자.


1. Model(모델)은 Controller(컨트롤러)나 View(뷰)에 의존하면 안된다.

먼저 Model의 하나로 개발된 Entity 중 사용자와 관련된 Model 역할을 담당할 Member라는 클래스 코드를 보자.

📄 Member.java

@ToString
@AllArgsConstructor
@Builder
@Getter
@NoArgsConstructor
@Where(clause = "deleted_at is NULL")
@SQLDelete(sql = "update member set deleted_at = CURRENT_TIMESTAMP where member_id = ?")
@Entity
public class Member extends BaseTimeEntity {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long memberId;

   @Column(nullable = false, length = 50, unique = true)
   private String email;

   @Column(length = 100)
   private String password;
   @Column(length = 50, unique = true)
   private String nickname;

   @Enumerated(EnumType.STRING)
   @Column(nullable = false)
   private Type type;
   
   @ElementCollection(fetch = FetchType.LAZY)
   @Builder.Default
   @Enumerated(EnumType.STRING)
   @Column(nullable = false)
   private Set<Role> role = new HashSet<>();

   private String picture;
   
   private String refreshToken;

   @Builder
   public Member(Type type, String nickname, String password, Set<Role> role, String picture) {
      this.type = type;
      this.nickname = nickname;
      this.password = password;
      this.role = role;
      this.picture = picture;
   }
}

Member라는 엔티티 클래스는 DB의 Member 테이블과 매핑되는 객체로 Model 중 한 부분을 차지한다.

Model은 Controller에게 전달받은 데이터를 조작하는 역할을 수행하기 때문에 해당 클래스 내부에 Controller나 View와 관련된 코드가 존재하면 안된다는 규칙을 생각하고 바라보자.

작성된 다양한 어노테이션과 속성들, 생성자를 살펴봐도 Controller나 View를 의존하고 있는 코드는 존재하지 않는다.

1. Model(모델)은 Controller(컨트롤러)나 View(뷰)에 의존하면 안된다.

이를 통해 1번 규칙은 어기지 않고 잘 지켜졌음을 확인하였다.


2. View(뷰)는 Model(모델)에만 의존해야 하고, Controller(컨트롤러)에는 의존하면 안된다.

필자는 여태까지 서버 애플리케이션과 클라이언트 애플리케이션을 별도로 개발해왔다. 그 중 대부분은 서버 애플리케이션으로 Spring Boot, 클라이언트 애플리케이션은 React로 기획하고 설계하였다. 그 이유는 먼저 View와 Model을 분리하여 보다 Restful한 API를 개발하기 위함이다.

클라이언트 사이드를 서버에서 함께 개발하는 방법 중 하나인 템플릿 엔진을 사용했다면 View 코드를 서버에서 함께 다뤄볼 수도 있었지만 Restful한 API 서버 개발을 지향하다보니 자연스럽게도 Spring Boot를 통해 개발한 서버 애플리케이션 코드에는 View에 관련된 코드는 작성하지 않았다.

이와 더불어, Restful한 API 서버 개발을 위해 요청에 대한 응답을 JSON으로 전달하는 ResponseEntity를 사용하기 때문에 1번 규칙에서 살펴보았듯이 Model 코드에서는 View와 Controller를 전혀 알지 못한다.

그렇게 View의 역할을 수행하는 React는 UI 컴포넌트를 구성하여 사용자 인터페이스를 표현하고, API 서버로부터 받은 데이터를 활용하여 화면을 렌더링하게 된다. 이 후 화면을 출력하기 위해 Spring Boot로 필요한 API를 요청하여 응답을 받고 받아온 데이터를 가지고 화면을 출력하게 된다.


그런데 여기서 문득 의문이 생겼다.

View가 Controller에게 의존하고 있는 건 아닐까?

React에서 API 요청을 Spring Boot의 서버로 전달하게 되면 요청을 처리할 수 있는 Controller가 동작하게 된다. 그렇다면 View가 Controller에게 의존하고 있는 건 아닐까? 라는 생각이 들었다.

🤔 React에서 API 요청을 보내면 서버의 Controller가 먼저 요청을 받는 것은 사실이지만, 그러나 View가 Controller에게 의존한다는 것을 의미하지는 않는다고 생각이 들었다.

React에서 API 요청을 보내어 데이터를 가져오는 과정에서 API 요청은 Controller의 역할을 수행하는 것이 아니라 Spring Boot 서버 애플리케이션에서 선언된 Controller로 전송되어 요청에 따른 작업을 수행하게 된다.

이 과정에서 View는 단순히 Model에만 의존하고 있지, Controller에 직접적으로 의존하고 있는 것은 아닐 것으로 생각이 들었다.

그래서 View에서의 API 요청으로 인해 Spring Boot의 Controller가 먼저 불려지지만, 이는 View에서 Model을 업데이트하기 위한 일부로 간주되기 때문에 View는 여전히 Model에만 의존하고 있다고 판단하게 되었다.


결국 View는 Model에게는 의존하지만 Controller에 대한 정보는 알지 못한다는 것을 알게 되었다.

따라서, Spring Boot를 통해 Model과 Controller의 역할을 수행할 Restful한 API 서버 애플리케이션을 개발하고, View의 역할을 수행하기 위해 React를 이용해 클라이언트 애플리케이션을 개발한다면 2번 규칙을 지키는데 수월하다고 생각이 들었다.

2. View(뷰)는 Model(모델)에만 의존해야 하고, Controller(컨트롤러)에는 의존하면 안된다.

2번 규칙 또한 잘 지켜진 것을 확인할 수 있었다.


3. View(뷰)가 Model(모델)로부터 데이터를 받을 때는 사용자마다 다르게 보여주어야 하는 데이터에 한해서만 받아야 한다.

마이페이지 화면에서 사용자의 포인트 출력을 담당하는 View 코드의 일부를 가져왔다.

📄 MyPage.tsx

const MyPage = () => {
  const { memberId } = useParams();
  useEffect(() => {
    const getUser = async () => {
      const userData = await MEMBER_API.getUser(Number(memberId));
      setUserInfo(userData.data?.data);
    };
  }
            
  return {
    <div className="font-light">
      포인트
      <span className="font-bold">{` ${userInfo?.point} `}</span></div>
  }
}

Mypage.tsx 코드를 살펴보면, View(뷰)인 MyPage 컴포넌트에서 MEMBER_API 요청을 하여 Model(모델)로부터 데이터를 받아오고 있다.

그리고 사용자마다 동일하게 보여주어야 할 포인트 라는 메뉴는 별다른 작업 없이 그대로 출력하고 있음을 알 수 있다.


그렇다면 MEMBER_API.getUser 메소드로 어떻게 데이터를 받아오는 것인지 추가로 살펴보자.

📄 apis.ts

export const MEMBER_API = {
  getUser: (memberId: number) => {
    return axiosInstance({ method: "GET", url: `/api/member/${memberId}` });
  }
}

MEMBER_API의 getUser를 보니 /api/member/${memberId} 경로로 GET 요청을 통해 데이터를 가져오게 된다.


이제 Spring Boot에서 해당 경로가 정의된 Controller를 살펴보자.

📄 ProfileController.java

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/member")
public class ProfileController {
    
    private final MemberService memberService;
    
    @GetMapping("/{memberId}")
    public ResponseEntity<ProfileGetResponse> getProfile(@PathVariable Long memberId) {
        ProfileGetResponse response = memberService.getProfile(memberId);
        return ResponseEntity.status(HttpStatus.OK).body(response);
    }
}

View에서 /api/member/${memberId}라는 API 요청에 대해서 ProfileController가 요청을 처리하여 ProfileGetResponse이라는 응답 데이터를 View에게 전해준다.


그러면 ProfileGetResponse에 사용자마다 다르게 보여줄 포인트 정보가 담겨있어야 한다.

📄 ProfileGetResponse.java

public class ProfileGetResponse {
	// ... 이하 생략
    private Long point;
}

ProfileGetResponse 코드의 내부를 보니 포인트 정보를 응답으로 포함하여 전달하고 있음을 결과적으로 알 수 있었다.

여기서 주목할 점은 사용자마다 다르게 포인트를 출력해주어야 한다는 것인데, 위의 MyPage 코드에서는 useParams 훅을 사용하여 memberId 값을 동적으로 받아오고 있다. 그렇게 받아온 memberId 값을 사용하여 사용자마다 다르게 보여주어야 하는 데이터를 가져와서 뷰를 렌더링하게 된다.

이를 통해 사용자마다 다르게 보여줄 데이터와 동일하게 보여주어야 할 데이터를 잘 구분하여 동작하고 있다는 것을 확인할 수 있었다.

3. View(뷰)가 Model(모델)로부터 데이터를 받을 때는 사용자마다 다르게 보여주어야 하는 데이터에 한해서만 받아야 한다.

3번 규칙도 큰 문제 없이 잘 지켜지고 있음을 확인하였다.


4. Controller(컨트롤러)는 Model(모델)과 View(뷰)에 의존해도 된다.

이번에는 MVC에서 Controller의 역할을 하는 MemberController 코드를 가져왔다.

📄 MemberController.java

@Log4j2
@RequiredArgsConstructor
@RequestMapping("/api")
@RestController
public class UserController {

    private final UserService userService;

    @PostMapping("/signup")
    public ResponseEntity<SecurityMemberDto> signup(@Valid @RequestBody SecurityMemberDto securityMemberDto) {
        return ResponseEntity.ok(userService.signup(securityMemberDto));
    }
    
    // ... 이하 생략
}

이 컨트롤러 클래스에서 signup이라는 메서드는 /api/signup 경로로 POST 요청이 올 때 호출되며, @RequestBody로 전달되는 SecurityMemberDto 객체를 요청으로 받게된다.

이 메서드 내부를 보면 @Valid 어노테이션을 사용하여 입력 데이터의 유효성을 검증하고, userService.signup() 메서드를 호출하여 회원가입 로직을 수행하게 된다.

마지막으로 ResponseEntity.ok() 메서드를 사용하여 회원가입이 성공할 경우, 200 상태 코드와 함께 처리 결과를 응답한다.

여기서, 위 코드는 Model에게로 데이터를 전해줄 SecurityMemberDto와 View로 데이터를 응답해줄 ResponseEntity를 사용하고 있음을 알 수 있다. 이는 결국 Model과 View에 적절히 의존하게 되어 Model과 View 간의 상호작용을 담당하는 역할을 수행하는 것이라고 생각하였다.

4. Controller(컨트롤러)는 Model(모델)과 View(뷰)에 의존해도 된다.

4번 규칙 또한 잘 지켜진 것을 확인할 수 있었다.


5. View(뷰)가 Model(모델)로부터 데이터를 받을 때는 반드시 Controller(컨트롤러)에서 받아야 한다.

메인 페이지 화면을 담당하는 View 코드 중 특정 데이터 목록을 출력하는 Main.tsx 파일 코드의 일부를 가져왔다.

3번 규칙에서 보았던 MyPage 코드도 해당 규칙의 예시로 설명할 수 있겠지만 다른 기능의 View 코드로 설명하였다.

📄 Main.tsx

const Main = () => {
  const [roomTop, setRoomTop] = useState([]);
  useEffect(() => {
    const getRoomNewest = async () => {
      const {
        data: { data },
      } = await ROOM_API.getRoomNew();
      setRoomTop(data);
    };
    getRoomNewest();
  }, []);

  return (
    <div className="md:w-[90%] md:m-auto">
      <div className="flex flex-col w-full md:grid md:grid-cols-2 md:gap-10 ">
        <MainList
          rooms={roomTop}
          option="room"
          sectionHeader={"최근 개설된 멘토링룸"}
        />
      </div>
    </div>
  );
};

export default Main;

위 코드의 Main 함수 내에서 return문을 통해 View의 역할을 수행하고 화면을 출력하게 되는데 화면을 출력할 때 ROOM_API 를 통해 Spring Boot의 Controller로 API 요청을 하게 된다.


데이터 요청 및 응답의 흐름을 살피기 위해 ROOM_API.getRoomNew() 메서드를 보자.

📄 apis.ts

export const ROOM_API = {
  getRoomNew: () => {
    return axiosInstance({ method: "GET", url: `/api/room/top` });
  },
};

해당 메서드에서는 /api/room/top 경로로 GET 요청을 하여 데이터를 받아오고 있다.


이제 Spring Boot 서버에서 Controller 코드를 보자.

📄 RoomController.java

@Log4j2
@RequiredArgsConstructor
@RequestMapping("/api/room")
@RestController
public class RoomController {

    private final RoomService roomService;
    
    @GetMapping("/top")
    public ResponseEntity<RoomListResponseDto> roomListTop10() {
        RoomListResponseDto response = roomService.getRoomTop10();
        return ResponseEntity.status(HttpStatus.OK).body(response);
    }
}

컨트롤러 내부를 보니 roomListTop10() 라는 메소드를 통해 /api/room/top라는 API 요청에 대한 데이터를 내려주고 있다.

이를 통해 Main.tsx라는 View에서 데이터를 받아오기 위해 Spring Boot에 정의된 Controller로 요청하여 데이터를 가져오고 있음을 확인할 수 있었다.

이를 통해 View에서 화면을 출력하기 위한 Model의 데이터를 Spring Boot에 정의한 Controller로 요청하여 받아온다는 사실은 누구나 이해할 수 있을 것이다.

5. View(뷰)가 Model(모델)로부터 데이터를 받을 때는 반드시 Controller(컨트롤러)에서 받아야 한다.

5번 규칙도 잘 지켜서 개발하고 있었다.


MVC 패턴의 규칙을 잘 지켰다고 좋은 코드를 작성한걸까?

앞에서 5가지 MVC 패턴의 규칙을 토대로 내가 개발했던 코드들을 리뷰할 수 있었다. 다행스럽게도 5가지 규칙을 어기지 않고 수행하고 있었음을 알 수 있었다.

MVC 패턴으로 프로젝트를 구축했다면 누구나 별다른 고민 없이도 자연스럽게 MVC 패턴의 규칙들을 지킬 수 있을 것이다.

그렇다면 MVC 패턴의 규칙들을 지켰다고 좋은 코드를 작성한걸까?

결론부터 말하자면 아니다 라고 답할 수 있다. 필자를 포함해서 MVC의 구조를 택하여 애플리케이션을 개발했던 사람이라면 알겠지만, MVC 패턴의 규칙을 따르는 것 말고도 좋은 코드를 작성하기 위한 규칙은 수없이도 많다.

일반적으로 MVC의 규칙을 잘 지킴으로써 얻을 수 있는 혜택은 코드 가독성, 유지보수성, 재사용성 등을 높이는 데 있다. 물론 좋은 코드에 대한 기준이 주니어 개발자, 시니어 개발자 등 여러 개발자마다 생각하고 판단하는 기준이 다르기에 딱 정해서 말하기는 어렵겠지만, MVC 패턴을 학습하면서 느꼈던 것은 MVC 패턴을 지키는 것만으론 부족할 수 있다는 것이다.

프로젝트를 개발하면서 단순히 MVC 구조로만 개발을 끝내는 것이 아니라 내부에 여러가지 요구사항을 이루기 위한 다양한 기술을 도입하게 될 것이다. 이러한 기술들을 다루기 위한 코드를 작성할 때, MVC의 규칙 뿐만이 아닌 복잡하고 어려운 규칙들을 지킬 수 있는 코드가 좋은 코드라고 필자는 생각하고 있다.


마치며

내가 개발했던 코드들을 리뷰하기 위해 MVC 패턴의 규칙들을 학습하고 이해하는 시간들을 통해 Model과 View, Controller의 역할과 이유, 목적에 대해서 분명하게 알 수 있었다.

또한, MVC 패턴의 구조를 가지는 프로젝트 개발에 임할 때 MVC의 흐름을 보다 빠르고 쉽게 이해할 수 있을 것이란 자신감도 생긴 것 같다.

가장 좋았던 점은 학습과 적용을 함께 진행하니 배운 것을 기반으로 내가 잘 알고 있는지 스스로 평가하고 판단할 수 있었고, 이를 팀원들과 공유하며 기술적으로 다양한 생각과 고민들을 들어볼 수 있었다.

앞으로도 MVC 패턴뿐만 아니라 다른 CS 지식들을 배울 때도 학습과 적용을 함께 진행하도록 할 예정이다.



이번 글에서 인용한 코드들은 아래 저장소에서 확인하실 수 있습니다.

잘못된 내용이 있다면 지적해주십시오. 다시 학습하여 정정하도록 하겠습니다.
profile
찍어 먹기보단 부어 먹기를 좋아하는 개발자

0개의 댓글