[Onboarding] : 객체 지향 프로그래밍은 처음이라

문승현·2022년 7월 5일
0

BeDev_1

목록 보기
3/7
post-thumbnail

'객체 지향 프로그래밍'은 프로그래밍을 공부하면서 많이 접해본 단어였다.
그러나 단순히 글로된 설명들만 보았었기에 정확히 내용을 이해하지 못하고 넘어가기 일쑤였다.
하지만 이번에는 직접 코드를 짜면서 객체 지향 프로그래밍에 대한 이해를 가질 수 있었다.

객체 지향 프로그래밍은 여러 가지 프로그래밍 패러다임 중 하나이다.
프로그래밍 패러다임은 크게 명령형 프로그래밍과 선언형 프로그래밍으로 구분할 수 있는데
객체 지향 프로그래밍은 그 중 명령형 프로그래밍에 속한다.
(명령형, 선언형 프로그래밍에 대한 내용은 아래 첫 번째 참고 자료가 도움이 되었다.)

객체 지향 프로그래밍의 중요한 특징으로는 캡슐화, 추상화, 상속, 다형성 등이 있다.

캡슐화(Encapsulation)

캡슐화란 데이터와 코드의 형태를 외부로부터 알 수 없게하고,
데이터의 구조와 역할, 기능을 하나의 캡슐 형태로 만드는 방법을 의미한다.
캡슐화를 하면 불필요한 정보를 감출 수 있어 정보 은닉을 할 수 있다.

추상화(Abstraction)

추상화는 객체의 공통적인 속성과 기능을 추출하여 정의하는 것을 의미한다.
클래스는 객체들이 어떤 특징들이 있어야 한다고 정의하는 추상화된 개념이다.

상속(Inheritance)

상속이란 기존 상위 클래스의 코드를 재사용 및 확장하는 것을 의미한다.
상위 클래스와 하위 클래스는 [하위 클래스 Is a kind of 상위 클래스] 관계가 성립한다.

다형성(Polymorphism)

다형성이란 한 객체가 상속을 통해 기능을 확장하거나 변경하여
다른 형태로 재구성되는 것을 의미한다.
오버로딩(Overloading)와 오버라이딩(Overriding)이
다형성의 대표적인 예라고 할 수 있다.

오버로딩은 사전적으로 ‘과적하다'라는 의미로
하나의 클래스 내에 사용하려는 이름과 같은 이름을 가진 메소드가 있더라도
매개 변수의 수나 타입을 다르게하여 새롭게 정의하는 것을 의미한다.

오버라이딩은 부모 클래스로부터 상속받은 메소드를
자식 클래스에서 재정의하는 것을 의미한다.
이때 오버라이딩하고자 하는 메소드의 이름, 매개변수, 리턴 값이 모두 같아야 한다.

정리하자면 객체 지향 프로그래밍에서는
어떤 대상을 추상화하여 공통점을 찾고 그것을 캡슐화하여 객체를 만든다.
그리고 해당 객체는 새로운 객체가 상속받을 수 있고, 기능을 수정 또는 추가할 수 있다.
결국 이런 과정을 통해 코드의 재사용성이 높아지고 유지보수가 편리해진다.

또한, 객체 지향 프로그래밍에는 SOLID 원칙이라는 것이 있다.
설명만 보고는 도통 무슨 의미인지 싶었으나
직접 코드를 짜다보니 '아하!' 하는 순간들이 있었다.

단일 책임 원칙(SRP, Single Responsibility Principle)

하나의 클래스는 하나의 책임만 가져야 한다는 원칙을 의미한다.
너무 많은 책임(기능)이 특정 클래스에 몰려있으면 해당 클래스를 나누는 것을 고려해야 한다.

개방 폐쇄 원칙(OCP, Open/Closed Principle)

확장에는 열려있으나 변경에는 닫혀 있어야 한다는 원칙을 의미한다.
특정 기능을 새롭게 구현한다고 하였을 때,
이로 인해 기존에 구현해 둔 것들이 영향을 받아서는 안된다.

리스코프 치환 원칙(LSP, Liskov Subsitution principle)

부모 클래스의 인스턴스를 사용하는 위치에서
자식 클래스의 인스턴스도 사용할 수 있어야하는 원칙을 의미한다.
예를 들어, 동물이라는 부모 클래스와 고양이라는 자식 클래스가 있을 때,
동물 클래스에서의 먹다라는 메소드와 고양이 클래스에서의 먹다라는 메소드가
형식적, 내용적 측면에서 원래의 형태와 의도를 위반하지 않아야한다.

인터페이스 분리 원칙(ISP, Interface Segregation Principle)

큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리시키는 원칙을 의미한다.
이를 통해 구현체들은 반드시 필요한 것들만 이용할 수 있어야 한다.

의존 관계 역전 원칙(DIP, Dependency Inversion Principle)

보통 상위 계층이 하위 계층에 의존하는 경우가 많으나 이러한 관계를 역전하여
하위 계층이 아닌 상위 계층에 의존해야 한다는 원칙을 의미한다.

아래는 API 구현 과정을 정리한 내용이다.
객체 지향에 대한 개념을 바탕으로 SOLID 원칙을 준수하고자 하였으나 어려운 부분이 많았다.
그럼에도 계속 Refactoring을 진행하며 발전시켜 나가보고자 하였다.

구현 과정

API를 구현 하면서 이전과 크게 달랐던 점은 여러 개의 레이어를 나누어 설계했다는 점이다.
사실 레이어를 나누지 않고 하나의 클래스에 모든 기능을 넣어서 코드를 작성할 수도 있었다.
그러나 단일 책임 원칙을 위해 하나에 클래스에는 하나의 기능만을 담고자 분리하였다.

우선, 컨트롤러와 서비스를 구분하였는데, 그 중 PostController를 먼저 구현하였다.
(구현하다보니 인터페이스 먼저 구현하는 것이 좀 더 편했을 것 같다는 생각이 들었다.)

컨트롤러는 아래와 같이 라우팅([Route("api/blog/post")])을 설정하여
클라이언트의 리퀘스트를 받아 리스폰스를 반환하는 역할을 담당하는 레이어이다.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace WebTutorial.Controllers
{
    [ApiController]
    [Route("api/blog/post")]
    public class PostController : ControllerBase
    {
    }
}

이후 컨트롤러에서 어떤 서비스를, 어떤 형태로 사용할 것인지 미리 설계할 필요가 있었다.
PostService를 사용하고자 해당 클래스의 인터페이스인 IPostService를 종속성 주입하였다.
종속성 주입은 별개의 게시글에서 다시 설명할 것이나,
객체 지향과 관련해서 이야기하자면 의존 관계 역전 원칙을 위한 기술이라 할 수 있을 것 같다.

using Microsoft.AspNetCore.Mvc;
using WebTutorial.Model;
using WebTutorial.Services;

namespace WebTutorial.Controllers
{
    [ApiController]
    [Route("api/blog/post")]
    public class PostController : ControllerBase
    {
        private readonly IPostService<PostDTO> _postService;
        public PostController(IPostService<PostDTO> postService)
        {
            _postService = postService;
        }
    }
}

이후 Post와 관련한 CRUD API를 작성하였다. 구현 자체는 아주 간단했다.
이것이 가능한 이유는 서비스 레이어가 따로 존재하기 때문이다.
따라서 Post를 CRUD 하는 세부 동작들은 컨트롤러에서 구현할 필요가 없고,
종속성 주입을 통해 사용이 가능해진 PostService의 메소드들을 호출하면 된다.

using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using WebTutorial.Model;
using WebTutorial.Services;


namespace WebTutorial.Controllers
{
    [ApiController]
    [Route("api/blog/post")]
    public class PostController : ControllerBase
    {
        // Dependency Injection by Constructor : IPostService 
        private readonly IPostService _postService;
        public PostController(IPostService postService)
        {
            _postService = postService;
        }    
                
        // Post related CRUD API Controller Implementation 
        [HttpPost]
        public async Task<Response<PostDTO>> CreatePostAsync([Bind("Title", "Content", "Author")] PostDTO post)
        {
            return await _postService.CreatePostAsync(post);
        }

        [HttpGet("{id}")]
        public async Task<Response<PostDTO>> ReadPostAsync(string pid)
        {
            return await _postService.ReadPostAsync(pid);
        }

        [HttpPut("{id}")]
        public async Task<Response<PostDTO>> UpdatePostAsync(string pid, PostDTO post)
        {
            return await _postService.UpdatePostAsync(pid, post);
        }

        [HttpDelete("{id}")]
        public async Task<Response<PostDTO>> DeletePostAsync(string pid)
        {
            return await _postService.DeletePostAsync(pid);
        }
    }
}

이후에는 PostController에서 사용한 IPostService와 PostService를 작성하였다.
IPostService는 인터페이스로서 아래와 같이 세부 구현 사항이 없는 메소드 목록만 작성하였다.
인터페이스 작성의 이유는 개방 폐쇄 원칙을 준수하기 위함이라 할 수 있다.
예를 들어, PostService와 유사하지만 별개의 Service를 만들고 싶다고 가정해보자.
IPostService를 상속받아 필요한 기능만 추가로 구현한다면 확장에는 열려있고,
PostService나 관련 코드들을 재작성하지 않아도 되기에 변경에는 닫혀있을 수 있다.

using System.Threading.Tasks;
using WebTutorial.Model;

namespace WebTutorial.Services
{
    public interface IPostService
    {
        Task<Response<PostDTO>> CreatePostAsync(PostDTO post);
        Task<Response<PostDTO>> ReadPostAsync(string id);
        Task<Response<PostDTO>> UpdatePostAsync(string id, PostDTO post);
        Task<Response<PostDTO>> DeletePostAsync(string id);
    }
}

세부 구현 사항은 IPostService를 상속한 PostService에서 아래와 같이 작성하였다.

using System.Threading.Tasks;
using WebTutorial.Database;
using WebTutorial.Model;

namespace WebTutorial.Services
{
    public class PostService : IPostService
    {   
        // Dependency Injection by Constructor : IDbRepository 
        private readonly IDbRepository _dbRepository;
        public PostService(IDbRepository dbRepository)
        {
            _dbRepository = dbRepository;
        }

        // Post related CRUD API Service Implementation 
        public async Task<Response<PostDTO>> CreatePostAsync(PostDTO post)
        {
            var newPost = new Post(post);
            await _dbRepository.CreateAsync(newPost.Pid, newPost);
            var response = new PostDTO(newPost);

            return Response<PostDTO>.Success(response);
        }

        public async Task<Response<PostDTO>> ReadPostAsync(string pid)
        {
            var currentPost = await _dbRepository.GetAsync<Post>(pid);
            if (currentPost == default)
                return Response<PostDTO>.Fail(ErrorCodes.NoDataWasFound, 404);

            var response = new PostDTO(currentPost);

            return Response<PostDTO>.Success(response);
        }

        public async Task<Response<PostDTO>> UpdatePostAsync(string pid, PostDTO post)
        {
            var currentPost = await _dbRepository.GetAsync<Post>(pid);
            if (currentPost == default)
                return Response<PostDTO>.Fail(ErrorCodes.NoDataWasFound, 404);

            var newPost = new Post(currentPost, post);

            await _dbRepository.SaveAsync(pid, newPost);
            var response = new PostDTO(newPost);
            
            return Response<PostDTO>.Success(response);
        }

        public async Task<Response<PostDTO>> DeletePostAsync(string pid)
        {
            var currentPost = await _dbRepository.GetAsync<Post>(pid);
            if (currentPost == default)
                return Response<PostDTO>.Fail(ErrorCodes.NoDataWasFound, 404);
            await _dbRepository.DeleteAsync(pid);

            return Response<PostDTO>.Success();
        }
    }
}

컨트롤러와 서비스에서 반환 값과 파라미터로 사용하는 것들 역시 작성해줄 필요가 있었다.
우선, 파라미터로 자주 사용되는 Post와 PostDTO를 작성하였다.
DTO라는 것을 처음 작성해보았는데, 이 역시 이전에는 사용해보지 않았던 내용이었다.

DTO를 사용하는 이유는 요청과 반환 시 사용되는 데이터를 제한하기 위함이다.
이를 통해 클라이언트에게 일부 속성을 숨길 수 있고 페이로드 크기 역시 줄일 수 있다.
즉, 보안과 성능 상의 이점을 위해 사용하는 것이라 할 수 있겠다.

using System;
using System.Collections.Generic;

namespace WebTutorial.Model
{
    public class Post
    {
        // Variables for Creating Pid which means Post ID
        private const string KeyPrefix = "post";

        public Post()
        {
        }

        public Post(PostDTO post)
        {
            this.Pid = KeyPrefix + Guid.NewGuid().ToString("N");
            this.Title = post.Title;
            this.Content = post.Content;
            this.Author = post.Author;
            this.CreatedAt = DateTime.Now;
            this.UpdatedAt = this.CreatedAt;
        }

        public Post(Post currentPost, PostDTO updatedPost)
        {
            Pid = currentPost.Pid;
            Title = updatedPost.Title;
            Content = updatedPost.Content;
            Author = updatedPost.Author;
            Comments = currentPost.Comments;
            CreatedAt = currentPost.CreatedAt;
            UpdatedAt = DateTime.Now;
        }

        public string Pid { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        public string Author { get; set; }
        public List<Comment> Comments { get; set; }
        public DateTime CreatedAt { get; set; }
        public DateTime UpdatedAt { get; set; }
    }
}
using System;
using System.Collections.Generic;

namespace WebTutorial.Model
{
    public class PostDTO
    {
        
        public PostDTO()
        {
        }
        public PostDTO(Post post)
        {   
            this.Pid = post.Pid;
            this.Title = post.Title;
            this.Content = post.Content;
            this.Author = post.Author;
            this.Comments = post.Comments;
            this.CreatedAt = post.CreatedAt;
            this.UpdatedAt = post.UpdatedAt;
        }

        public string Pid { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        public string Author { get; set; }
        public List<Comment> Comments { get; set; }
        public DateTime CreatedAt { get; set; }
        public DateTime UpdatedAt { get; set; }
    }
}

반환 값으로는 Response struct를 사용하였는데, 여기에보면 <T>라는 것이 있는데
이는 Generic Type(하나의 값이 여러 다른 데이터 타입들을 가질 수 있는 기술)을 의미한다.
처음 사용해본 것이었는데 코드 작성 시 유연함과 편리함을 동시에 제공해주었다.

namespace WebTutorial.Model
{
    public struct Response<T>
    {
        public const string DefaultError = "error";

        public string Error { get; set; }
        public int Status { get; set; }
        public T Data { get; set; }

        public static Response<T> Success(T data = default)
        {
            return new Response<T>
            {
                Error = "No Errors Found",
                Status = 200,
                Data = data
            };
        }

        public static Response<T> Fail(string error = DefaultError, int statusCode = 404)
        {
            return new Response<T>
            {
                Error = error,
                Status = statusCode,
                Data = default
            };
        }

        public bool IsSuccess() => Error is null;
        public bool IsError() => Error is not null;
    }
}

지금까지 API 구현 과정에 대한 설명이었다.
중간 중간 객체 지향 프로그래밍의 원칙들이 어떻게 적용되었는지도 적어보았다.
정확하지 않은 부분도 있을 수 있기에 살짝은 걱정되지만
그래도 뭔가를 새로 배우고 적용해보았다는 점에서 뿌듯한 점이 있다.

참고 자료 1) - Introduction of Programming Paradigms
참고 자료 2) - Declarative vs imperative
참고 자료 3) - Differences between Procedural and Object Oriented Programming
참고 자료 4) - Functional Programming Paradigm
참고 자료 5) - What is object-oriented programming? OOP explained in depth
참고 자료 6) - S.O.L.I.D. Principles of Object-Oriented Programming in C#
참고 자료 7) - Tutorial: Create a web API with ASP.NET Core
참고 자료 8) - Handle requests with controllers
참고 자료 9) - Routing to controller actions
참고 자료 10) - C# Generic

0개의 댓글