개발하다가 궁금한 게 생겼는데요.

.NET6 기반의 웹서비스를 만드는데 서버에서 파일 업로드/다운로드 관련 비즈니스 로직을 만드는데 이 구성에 대해 조금 궁금한 게 생겼습니다.

기존 닷넷프레임워크 4.x를 사용할 때는 그냥 FileUtil.cs를 만들고 static class로 만들어서 전역에서 자유롭게 사용할 수 있게 만들었었는데요.

닷넷코어로 만들면서 굳이 이걸 static class로 해서 전역으로 만들 필요가 있나… IFileService를 만들고 DI로 하면 더 닷넷코어스러운(?) 코드를 만드는 게 아닌가 싶어서요. 둘다 상관없는 방식이겠으나 갑자기 다른 분들의 생각은 어떤지 궁금합니다.

10개의 좋아요

닷넷 코어로 오면서 DI 패턴으로 많이들 하시는거 같아요.
인터넷에 나오는 대부분의 Tutorial도 그렇게 설명하고 있고요.

8개의 좋아요

static 서비스는 Singleton 으로 등록된 서비스와 거의 같습니다.

그러나, static 서비스의 가장 큰 단점은 "추상으로 정의할 수 없다"는 점입니다.
그 결과로, 나의 코드는 서비스와 강하게 결합되고 이는 유지보수 측면에서 매우 치명적입니다.

FileUtil.cs 의 호출이 한 100군데 정도 널리 사용되었고, 나중에 서비스의 코드에 변경이 있으면, 어지간한 편집증이 있지 않고서야 손대기 힘들죠.

DI는 추상에 의존을 가능하게 해줘, 이러한 문제를 해결합니다.

서비스를 등록할 때, AddStatic 이 아니라, AddSingleton 인 이유가, static 과 singleton은 비슷한 수명주기를 갖는 서비스라도, 근본적으로 다른 접근 방식이기 때문입니다.

11개의 좋아요

동작하도록 만드는 것이 목표라면 어느쪽이든 상관은 없겠지만, @BigSquare 님의 말씀대로 구현과 인터페이스를 분리해서 TDD를 가능하게 해주기 때문에 기술 부채를 줄이는데 도움이 됩니다.

코드 구현 분리가 안되면, 테스트를 할 때마다 매번 실제 구현체가 제대로 동작하도록 입력을 시뮬레이션해주고, 출력을 만드는 과정을 신중하게 고려해야하기 때문에 테스트 비용이 높아질 개연성이 커지기 때문이겠고요! (이런 식이면 코드를 단위로 나누어서 테스트하는 것이 무척 피곤한 일이 될겁니다.)

하지만 테스트해야 할 코드의 규모나 실행 비용이 그다지 높지 않다면 모든 것을 DI로 대입해야만 하는 것 또한 아니기 때문에 맥락에 따라 적절한 구현 방식을 고르는 요령도 필요하다고 생각합니다.

8개의 좋아요

저는 IoC Container를 쓰는 것에 찬성입니다.

물론 IoC Container를 사용하는 것에 반대하시는 견해들도 있습니다.

사전에 타입을 등록하지 않고 생성자 주입을 받을 경우 주입되지 않은 타입에 대해 인스턴스를 생성할 수 없기 때문에 프로세스에 대놓고 런타임 에러가 나는 부분이있기 때문인데요.

객체지향이란 여러 서적들에서 말하듯, 트레이드오프의 개념이 중요하다고 합니다.
이건 결합 vs 응집 사이에서 열심히 머리를 굴리는 것이 객체지향이라는 뜻인데요.

결합은 직접 참조를 말합니다.
이 경우 디버깅이 용이하다는 장점이 있습니다.
응집은 직접 참조하지 않고 인터페이스를 통해 참조합니다.
즉, 참조가 인터페이스에 걸려있고 실제 객체에 걸려있지 않습니다.
따라서 실제 클래스에 변형을 가해도 사용하는 곳에서는 인터페이스를 참조하고 있기 때문에 문제가 없습니다. (이 부분이 사용하시는 분에 따라 호불호가 갈리는 부분입니다)

객체지향의 확장판이라고 생각하는 DDD(도메인 주도 설계)에서는 도메인끼리 응집하라고 합니다.
같은 업무를 하는 프로세스끼리의 도메인 이기도 하겠지만, 이것을 코드에 접목해보면 인터페이스를 이용하여 개발했을 때 결합도를 낮추고 응집도를 높이는 효과가 있습니다.
물론 너무 심하게 추상화를 해버리면 개발 인원이 적을 경우 디버깅에 불편을 겪을 수 있기 때문에 적당한 추상화가 좋다고 생각합니다.

IoC Container의 Inversion of Control 가 나오기 이전부터, 이미 객체지향 개발의 아이덴티티인 SOLID 패턴의 마지막 패턴, DIP가 존재했습니다.
객체지향의 꽃은 의존성과 추상화를 어떻게 다루느냐 인데, 인터페이스는 이 두 가지를 적절하게 해결해 줄 수 있는 방법입니다.
추상화를 통해 객체를 클래스로, 클래스를 인터페이스로 추상화하여 중복소스코드를 줄이고, 강한 타입 참조를 제거하여 유지보수에 대한 유연성을 제공합니다. (물론 제공할 필요가 없는 곳에서는 굳이 안해도 됩니다.)

여기서 IoC Container와 Interface를 함께 사용하면 구현 은닉과 의존성을 둘 다 챙길 수 있습니다.
IoC Container를 사용하면 통상 생성자 주입을 통해 타입을 제공 받게 되는데, 생성자의 parameter가 적다면, IoC Container의 이점을 크게 못 누릴 수 있지만, 엄청나게 많다면, IoC Container의 장점을 제대로 누릴 수 있습니다.

예를 들면,

public class A {}
public class B {}
public class C {}
public class D {}
public class E {}
public class F {}
public class G {}

public class Alphabet
{
    public alphabet(
        A a,
        B b,
        C c,
        ...
    }
}

Alphabet alphabet = new(
    new A(),
    new B(),
    new C(),
    ...
);

위와 같이 생성해야만 합니다.
여기서 A B C D E F G class가 생성자가 각각 3개씩 있게 바뀐다면…우리는 코드를 이곳저곳을 많이 수정해줘야합니다.

IoC Container를 사용한다면

service.AddTransient<A>();
service.AddTransient<B>();
service.AddTransient<C>();
service.AddTransient<D>();
service.AddTransient<E>();
service.AddTransient<F>();
service.AddTransient<G>();
service.AddTransient<Alphabet>();

Alphabet alphabet = ServiceCollection.GetService<Alphabet>();

이렇게 되어버리니 생성자 수정으로 인해 코드를 수정하는 일이 적어집니다.
게다가 클래스를 직접 등록하는 게 아니라 인터페이스를 등록한다면,

public interface IA {}
public interface IB {}
public interface IC {}
public interface ID {}
public interface IE {}
public interface IF {}
public interface IG {}

public class A : IA {}
public class B : IB {}
public class C : IC {}
public class D : ID {}
public class E : IE {}
public class F : IF {}
public class G : IG {}

public interface IAlphabet {}

public class Alphabet : IAlphabet
{
    public Alphabet(
        IA a,
        IB b,
        IC c,
        ...
    )
}


service.AddTransient<IA, A>();
service.AddTransient<IB, B>();
service.AddTransient<IC, C>();
service.AddTransient<ID, D>();
service.AddTransient<IE, E>();
service.AddTransient<IF, F>();
service.AddTransient<IG, G>();
service.AddTransient<IAlphabet, Alphabet>();

IAlphabet alphabet = ServiceCollection.GetService<IAlphabet>();

이렇게 하면 코드가 변경되면서 발생하는 모든 의존성이 다 해결되게 됩니다.
물론 이렇게 했을 때 인터페이스에 참조가 걸리기 때문에, 인터페이스와 DI에 익숙하지 않은 사람이라면 디버깅이 어려울 수 있습니다.
하지만 인터페이스라는 것은 쓰면 쓸 수록 잘 쓸 수 있게 되고, 추상화를 위해 꼭 필요한 도구이며, 코드만 많아지는 쓸모없는 기능이라고 생각이 안 들게되며, 협업이 반드시 필요한 기능으로 느껴질 것이라고 생각합니다.


다 쓰고 댓글을 달고 다시 봤더니, 글의 요지는 Interface와 상관이 없는 내용인데 IoC Container에 대해 설명하다가 의존성 얘기를 하면서 Interface 삼천포로 빠져버리는 설명이었군요…;

아무튼…결론은 저는 찬성입니다…!

16개의 좋아요

추상 클래스가 이글에 슬퍼요를 눌렀습니다. ^^

8개의 좋아요

너무 유익한 댓글들을 달아주셔서 말씀해주신 내용들의 개념들에 대해서도 다시 한 번 생각하고 되새겨보게 되었네요~ 너무 감사합니다 :+1: :+1:

8개의 좋아요

추상클래스는 다중상속이 안되는 개념이라…
인터페이스도 다중상속은 아니고 사실은 다형성제공…

다형성제공일때 이점을 좀 봐왔던 경험을 해와서 저도 둘 다 사용하지만 의존성에 관해선 추상클래스보단 인터페이스가 적합하다고 생각합니다!

아 그런다고 추상클래스를 안쓰지는 않습니다. 저도 둘 다 사용합니다!

아 추가로…제네릭을 사용할 때 인터페이스가 공변성과 반공변성을 제공하기 때문에 추상화에서 추상클래스보다 사용처가 높은 것 같다는 개인적인 의견도 있습니다!

6개의 좋아요

다들 누구나 한번 쯤 피상적으로 생각만 하고 있다고, 이런 기회로 한번씩 되새겨 보는 것이죠. 질문을 한 사람도 도움을 받고, 준 사람도 도움을 받고…
말은 번지르르하게 해도, 여전히 static class 로 작성한다는… ㅠㅠ

8개의 좋아요

아키텍쳐에 따라, 인터페이스가 필요한 곳과 추상 클래스가 필요한 곳이 따로 있다고 생각합니다.
예를 들어, MVVM에서 뷰모델은 인터페이스보다는 추상 클래스로 추상화하는 것이 더 적합한 경우라고 생각합니다.
직접 주입하든, 서비스 컨테이너가 대신 주입해주든.

5개의 좋아요

네네 저도 사용처가 다르다고 생각합니다.

그래서 저도 둘 다 사용하는것이구요…

일단 인스턴스가 실제로 없는 클래스는 추상클래스로 해서 클래스를 확장하는 형태로 하고 있습니다.

물론 그 추상클래스도 여러 인터페이스의 다형성제공을 받을거구요~

말씀하신 뷰모델 추상 클래스에 대해선 대표적으로 inotifypropertychanged를 구현한 추상클래스를 뷰모델이 상속 받는 방법이 있을 것이고 저도 communitytoolkit으로 넘어가기 전까지는 자주 사용했었습니다!!

5개의 좋아요

천재들 앞에서는 공허한 메이리에 불과하다는 ^^.

그런데, 말씀 중에 저도 고민하던 부분이 있네요.
추상 클래스가 인터페이스를 상속하는 2중 추상화가 과연 실용적인가라는 의문이 항상 있었거든요.
코드의 복잡도만 올리는 것은 아닌지…

6개의 좋아요

저도…그것에 대해 고민하고 있고 지금도 어려운 문제인데, 쓰다보니까 현재 시점의 지적수준에서 도달한 결론은,

모듈화의 관점에서 보면 그냥 다 다른 객체들이 서로 공유할 부분만 공유하면서 사이버 세상을 살아가는 구나…하고 생각하게 되었습니다.

추상클래스와 인터페이스의 명확한 차이는 추상클래스에선 기본 구현이 있다는 것이고 인터페이스는 기본 구현이 없는 것인데, (C# 8.0부터는 private 맴버 한정하여 기본 구현 가능) 기본구현이 없는 껍데기를 과연 어디다 써야하는지 고민하다가, json 모델들을 추상화하다가 조금 느끼게 되었습니다.

slack bot을 .net으로 만들면서 가장 먼저해야했던건 slack의 메세지 추상화였는데 런타임 관점에서 접근하다가 느낌이 좀왔었습니다.

인터페이스로 추상화한 객체를 사용할 때 인터페이스로 사용하면 컴파일 타임인 코드를 칠때는 인터페이스에서 정의한 기능 바깥 부분을 사용할 수 없지만 실제로는 인스턴스 전부 있는 것이기 때문에 형변환을 쉽게할 수 있게했습니다.

제가 지금 모바일이라…코드 예시를 할 수 없는 점…은 내일 수정하도록 하겠습니다 ㅠㅠ

6개의 좋아요
namespace SlackApi
{
    public interface ISlackBlocks
    {
#if NET6_0_OR_GREATER
        string Type { get; init; }
#else
        string Type { get; set; }
#endif
        string BlockId { get; set; }
    }
}
namespace SlackApi.Blocks.Interfaces
{
    public interface IActions<T> : ISlackBlocks
    {
        List<T> Elements { get; set; }
    }
}
namespace SlackApi.Blocks.Interfaces
{
    public interface ISection<T> : ISlackBlocks
    {
        TextObject Text { get; set; }
        List<TextObject> Fields { get; set; }
        T Accessory { get; set; }
    }

    public interface ISection : ISlackBlocks
    {
        TextObject Text { get; set; }
        List<TextObject> Fields { get; set; }
    }
}
namespace SlackApi.Blocks
{
    /// <summary>
    /// https://api.slack.com/reference/block-kit/blocks#section_fields
    /// </summary>
    public sealed class SectionBlock<T> : ISection<T>
    {
        public SectionBlock()
        {
            Type = "section";
        }

        [JsonProperty("type")]
#if NET6_0_OR_GREATER
        public string Type { get; init; }
#else
        public string Type { get; set; }
#endif

        [JsonProperty("text", NullValueHandling = NullValueHandling.Ignore)]
        public TextObject Text { get; set; }

        [JsonProperty("block_id", NullValueHandling = NullValueHandling.Ignore)]
        public string BlockId { get; set; }

        /// <summary>
        /// 최대 10개
        /// </summary>
        /// <value></value>
        [JsonProperty("fields", NullValueHandling = NullValueHandling.Ignore)]
        public List<TextObject> Fields { get; set; }

        [JsonProperty("accessory", NullValueHandling = NullValueHandling.Ignore)]
        public T Accessory { get; set; }
    }

    public sealed class SectionBlock : ISection
    {
        public SectionBlock(string textType, bool isText = true)
        {
            Type = "section";

            if (isText)
                Text = new TextObject(textType);
            else
                Fields = new List<TextObject>();
        }

        [JsonProperty("type")]
#if NET6_0_OR_GREATER
        public string Type { get; init; }
#else
        public string Type { get; set; }
#endif

        [JsonProperty("text", NullValueHandling = NullValueHandling.Ignore)]
        public TextObject Text { get; set; }

        [JsonProperty("block_id", NullValueHandling = NullValueHandling.Ignore)]
        public string BlockId { get; set; }

        /// <summary>
        /// 최대 10개
        /// </summary>
        /// <value></value>
        [JsonProperty("fields", NullValueHandling = NullValueHandling.Ignore)]
        public List<TextObject> Fields { get; set; }
    }
}
namespace SlackApi.Blocks
{
    /// <summary>
    /// https://api.slack.com/reference/block-kit/blocks#actions_fields
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public sealed class ActionsBlock<T> : IActions<T>
    {
        public ActionsBlock()
        {
            Type = "actions";
        }

        [JsonProperty("type")]
#if NET6_0_OR_GREATER
        public string Type { get; init; }
#else
        public string Type { get; set; }
#endif

        [JsonProperty("block_id", NullValueHandling = NullValueHandling.Ignore)]
        public string BlockId { get; set; }

        [JsonProperty("elements")]
        public List<T> Elements { get; set; }
    }
}
[Theory]
    [InlineData("token", "!키워드")]
    public async Task ChatPostMessageTest(string oauthUserKey, string requestString)
    {
        string BuildingDBTemplateSearchMessageBlock(string userDisplayName, string[] request, DataTable response)
        {
            List<ISlackBlocks> blocks = new();
            blocks.Add(new SectionBlock<PlainTextElement>
            {
                Text = new DefaultTextObject
                {
                    Text = $"{userDisplayName}님, 요청하신 결과입니다.",
                    Emoji = true
                }
            });
            blocks.Add(new SectionBlock<PlainTextElement>
            {
                Text = new DefaultTextObject
                {
                    Text = $"Database: {request[1]}",
                    Emoji = true
                }
            });
            blocks.Add(new SectionBlock<PlainTextElement>
            {
                Text = new DefaultTextObject
                {
                    Text = $"Table: {request[2]}",
                    Emoji = true
                }
            });
            blocks.Add(new SectionBlock<PlainTextElement>
            {
                Text = new DefaultTextObject
                {
                    Text = $"Key: {request[3]}",
                    Emoji = true
                }
            });
            blocks.Add(new SectionBlock<PlainTextElement>
            {
                Text = new DefaultTextObject
                {
                    Text = $"Value: {request[4]}",
                    Emoji = true
                }
            });
            blocks.Add(new DividerBlock());

            foreach (DataRow dr in response.Rows)
            {
                foreach (DataColumn dc in response.Columns)
                {
                    blocks.Add(new SectionBlock<PlainTextElement>
                    {
                        Text = new DefaultTextObject
                        {
                            Text = $"{dc.ColumnName}: {dr[dc]}",
                            Emoji = true
                        }
                    });
                }
            }

            string blockMessage = JsonConvert.SerializeObject(blocks);

            return blockMessage;
        }

        HttpClient httpClient = new();

        DataTable dt = new();
        string blockMessage = BuildingDBTemplateSearchMessageBlock("vincent", requestString.Split(' '), dt);

        ReqChatPostMessage reqChatPostMessage = new();
        reqChatPostMessage.SetQueryString(
            channel: "채널코드",
            blocks: blockMessage);
            
        IEntity<ResChatPostMessage> testResult = await reqChatPostMessage.RequestStartAsync(httpClient, oauthUserKey);
        
        Assert.True(testResult.Result.Ok);
    }

위 코드는 제가 만든 사내 Slack bot의 boilerplate의 일부를 수정한 것입니다.
Interface로 정의했던 코드를

List<ISlackBlock> blocks = new();

위에 모두 담아서 json 직렬화를 수행하면 인스턴스가 모두 보이게 되는데, 코드상에서는 interface에 있는 property만 사용할 수 있기 때문에… 이런 interface에서 추구하는 지연 바인딩 의 개념이 좋았습니다.

추상클래스는 상속을 목적으로, 인터페이스는 다형성제공을 목적으로 한다는 것이 쓰면 쓸 수록 이해도가 깊어져 갔습니다.

코드 예시는 제가 확 와닿았던 예시라서 보시는 분들은 안 와 닿으실 수 있을 것 같습니다…ㅠ

이 글을 보시는 많은 분들이 interface에 대해 느낌이 오는 코드를 만나시기를 바랍니다…!

5개의 좋아요

저는 좀 다른 의견을 가지고 있습니다.
(아래의 글은 시비를 거는 것이 아니라, 건전한 논쟁을 위한 것임을 먼저 밝힙니다)

다형성을 달성하는 방법은 상속과 인터페이스 모두입니다.
이들의 차이점은 상속은 동일한 정체성을 가진 객체들 사이의 다형성이고, 인터페이스는 정체성이 다른 객체들 사이의 다형성입니다.

저는 상속을 통한 다형성을 “수직적 다형성” - 세대를 지나면서 행위가 달라지는 -,
인터페이스를 통한 다형성은 “수평적 다형성” - 동일 족속이 아닌 객체사이의 동일 행위 -으로 부릅니다.

정체성의 다른 말은 고유성이고, 고유성은 코드에서 “ID” 속성으로 표현됩니다.

보여주신 예시에서, ActionBlock 이든, SectionBlock 이든 이들은 모두 SlackBlock이라는 족속입니다.
왜냐하면, 이 객체들은 SlackBlock의 ID (Primary Key) 를 통해 각자의 고유성을 보장받기 때문입니다.

직계 조상을 인터페이스로 정의하는 것은, C#에서 금지하고 있는 복수의 기반 객체를 파생하는 것이기 때문에, 오류라고 보고 있습니다.

예제의 코드는 다형성의 원칙에 입각한 설계라기 보다는, 직렬화 도구의 허접함으로 인해 어쩔 수 없는 선택을 보여준 예시같습니다. 아마 Newton의 Json 시리얼라이저가 polymorphic serialization 을 지원하지 않기 때문에 발생한 일인 것 같습니다.

다형성의 원칙을 지키면서도, 직렬화 도구의 모자람은 dynamic 키워드가 채울 수 있을 것 같습니다.
(아래의 코드는 .Net 6.0 을 대상으로, System.Text.Json 에 의존하고 있기 때문에, 대상 프레임워크와 의존성에 따라 결과가 달라질 수 있습니다)

using System.Text.Json;
using System.Text.Json.Serialization;

public abstract class SlackBlock {
    [JsonPropertyName("type")]
    public string? Type { get; set; }

    [JsonPropertyName("block_id")]
    public string? BlockId { get; set; }

    public dynamic CurrentInstance() => this; 
}

public class SectionBlock : SlackBlock {

    [JsonPropertyName("text")]
    public TextObject? Text { get; set; }

    [JsonPropertyName("fields")]
    public List<TextObject> Fields { get; set; } = new();
}

public class SectionBlock<T> : SectionBlock {

    [JsonPropertyName("accessory")]
    public T? Accessory { get; set; }
}

public class TextObject {

    [JsonPropertyName("text")]
    public string? Text { get;  set; }

    [JsonPropertyName("emoji")]
    public bool? Emoji { get; set; }
}

사용코드

        SlackBlock sb = new SectionBlock<object>() {
            Type = "myType",
            BlockId = "001",
            Text = new TextObject() { Text = "고객님", Emoji = true, },
            Accessory = null,
        };

        var json = JsonSerializer.Serialize(sb.CurrentInstance());
        Console.WriteLine(json);

결과

{"accessory":null,"text":{"text":"\uACE0\uAC1D\uB2D8","emoji":true},"fields":[],"type":"myType","block_id":"001"}
6개의 좋아요

좋은 의견 감사드립니다!

직계 조상을 인터페이스로 정의하는 것은, C#에서 금지하고 있는 복수의 기반 객체를 파생하는 것이기 때문에, 오류라고 보고 있습니다.

저는 이 의견에서 인터페이스를 조상이라고 생각하지 않습니다.

C#이나 문법적인 편의상으로

public class Alphabet : 추상클래스, 클래스, 인터페이스

이렇게 동작하지, java만 봐도 상속에서는 extends를, interface는 implements라는 키워드를 사용하기 때문입니다. 그래서 제가 인터페이스에 대해서 조상이나 상속이라는 표현을 꺼리는 것입니다.

말씀하신 것 중에서 제가 공감하고, 앞으로 저도 사용할 키워드라고 생각되는 부분은 아래 부분입니다. 좋은 키워드 감사드립니다 ㅎㅎ

저는 상속을 통한 다형성을 “수직적 다형성” - 세대를 지나면서 행위가 달라지는 -,
인터페이스를 통한 다형성은 “수평적 다형성” - 동일 족속이 아닌 객체사이의 동일 행위 -으로 부릅니다.

또한 dynamic도 설계의 유연함에 따라 도입할 수 있는 충분한 여지라고 생각합니다.
하지만, dynamic을 사용하면 너무 유연하다 못해 오타같은 휴먼에러를 낼 수 있는 여지가 다분하다고 생각하여 개인적으로는 아직 수준 미달이라 사용을 일부러 꺼리고 있습니다.
위와 같은 Modeling에서 인터페이스와 제네릭을 이용하면 정적인 코딩으로 IDE가 잡아줄 수 있기 때문입니다.

그리고 말씀하신 직계 조상을 인터페이스로 정의하는 것이 오류라는 부분은, 애초에 Generic 객체들의 최고 높은 추상화는 IEnumerable 입니다.

IEnumerable로 IList나 ICollection 이런 것들이 나오니까요…그래서 저는 이런 반증으로 설계의 오류라고 생각하지 않으며 첫 의견과 동일한대로…추상 클래스던, 인터페이스던 상황에 맞게 챙기면 좋다고 생각하고 있습니다.

건강한 토론 문화 만들어 주셔서 감사합니다!


아 그리고 저 프로젝트에서 interface를 도입해서 해결할 수 밖에 없는 이유가 하나 있었는데, ISlackBlock은 제가 임의로 만들었지만, Slack 문서 상 Section과 Action의 특징들이 하나의 Mesasge안에 복합적으로 합쳐서 만들 수 있도록 설계되어 있었기 때문입니다. 그래서 여러 개의 특징(특징을 인터페이스라고 볼 수도 있을 것 같습니다)을 동시에 가질 수 있는 수단이 필요했었습니다. (Section과 Action은 Slack 문서상에 있는 공식 단어이며, Section과 Action외에도 다른 특징들이 있는 단어들이 있습니다.)

클래스 설계의 최초 조상은 무조건 부정할 수 없는 fact로 object 타입이기 때문에…저도 클래스에 대해서는 상속과 조상이라는 표현을 사용하고 있습니다.
어차피 object를 다 상속하고 있고 편의에 따라 인터페이스를 만들거나 기존것을 제공하여 수정하는 것이라서, 클래스 들의 직계조상은 클래스 형태가 맞는 것 같습니다 ㅎㅎ

8개의 좋아요

좋게 봐주셔서 감사합니다.

추가로 덧붙이자면,

언어를 떠나, 수단(언어)을 객체 지향적 개념에 맞지 않게 사용했을 때 문제가 발생하죠. Agile 원칙에 관한 에서 예시로 든 모든 문제 유형들은 문법 자체에 문제가 있거나, 문법이 틀려서 발생한 것이 아니라, 문법을 개념과 어긋나게 써서 발생한 것들이죠.

"프로그래머의 인식에 비해 과도한 도구를 휘두르고 있다"고 누가 표현하던데… 기억은 안나네요. ^^

5개의 좋아요

언어를 떠나, 수단(언어)을 객체 지향적 개념에 맞지 않게 사용했을 때 문제가 발생하죠. Agile 원칙에 관한 에서 예시로 든 모든 문제 유형들은 문법 자체에 문제가 있거나, 문법이 틀려서 발생한 것이 아니라, 문법을 개념과 어긋나게 써서 발생한 것들이죠.
"프로그래머의 인식에 비해 과도한 도구를 휘두르고 있다"고 누가 표현하던데… 기억은 안나네요. ^^

공감합니다…
어차피 인터페이스던 추상클래스던 비지니스로직이 워낙 간단하고 추상화가 필요 없는 동네에서는 전혀 쓰일 일이 없다고 생각합니다.

제 경우에는 저도 공장 업계로 시작을 했을 때 저런 추상화 기능을 전혀 사용하지 않다가 일반 서비스 업종으로 넘어오고 협업을 하면서 저런 것을 접했습니다.
그제서야 비로소 추상화를 해야겠구나! 하는 니즈가 생겼고 중복 소스코드를 더 현대적이고 고급스럽게 캡슐화 할 수 있지 않을까 라고 생각해서 사용해보고 싶었고, 또한 동년차 다른 도메인의 개발자들은 이미 이런 추상화 개념을 사용하고 있을텐데…라는 생각이 들게 되면서 다른 개발자들과 얘기를 할 때 추상화를 못한다면 기술적으로 좀 후달려보이겠구나;; 하는 생각이 있어서 공부를 하고 처음 시도할 때는 시행착오로 …배보다 배꼽이 더 크게 사용했었습니다.

지금도 고수들이 봤을 때 잘 사용하고 있는지는 모르겠습니다.
그냥 나름의 커리어로, 나름의 정리된 개념으로, 객체지향에서 가장 중요하다고 생각하는 적당한 추상화 에 대해 트레이드오프 어딘가에서 적절히 선택하고 있을 뿐입니다…ㅎㅎ
결국 협업과 일이 중요한 것이고, 필요에 의해 추상화가 될뿐, 추상화가 우선시되면 안된다는 생각이 있기 때문입니다.
그리고 협업 간 팀원들의 지적수준도 모두 맞아야하구요.

말씀하신 Agile 이론에서도 포커스는 협업입니다.
제가 java를 예시로 들면서 저의 생각을 밝히긴 했으나, 팀에서 추상화를 추상 클래스로 하기로 했고, 인터페이스를 상속의 개념으로 사용하기로 했다면, 의견은 제시할 수 있겠으나 협업에 방해되지 않게 순응하고, 적응하고, 제 의견을 섞어가면서 사용했을 것입니다.

그리고 마지막 댓글을 달았던 의도는, 어차피 말씀하신대로 클래스나 인터페이스를 어떻게 설계하던 이미 object를 상속받고 있으므로, interface를 만들어서 제 코드 상에서 최초의 직계조상이라고 하더라도 문제가 없는 것 아닐까 하는 의견이 었습니다.
그냥 제가 만들고 싶어하는 특징일 뿐이죠.

그런 의미에서 제 소스코드 예시에서는 Slack Message를 구현하기 위해 제가 class를 따로 만들지 않고 기존의 List를 이용해 만들었으며, interface의 특징을 이용해 Messaging을 편하게 하기 위해 ISlackBlock이라는 인터페이스를 이용했다고 정리할 수 있을 것 같습니다.
결국 말씀하신 직계 조상에 대한 내용이 제가 보기에는 제 소스코드 예시에 없다는 것이지요…ㅎㅎ

6개의 좋아요

오옷! 추상클래스와 인터페이스 토론이군요ㅇㅅㅇ! 꿀잼각ㅋㅅㅋ

저도 여기 살짝 거들어 보자면…

추상클래스와 인터페이스 모두 다형성 제공이라는 목적을 가지고는 있지만
둘의 방향성은 약간 다릅니다.

추상클래스(일반 클래스를 포함하여) 는 말 그대로 상속과 파생의 개념을 통하여
부모 타입의 내용을 물려받고 자식 클래스는 부모의 타입으로 사용될 수 있음을 의미 합니다.
이것은 전통적인 타입 시스템을 가진 언어에서 일반적인 형태이죠.
타입 간 부모 자식 관계를 형성하는 방식으로 다형성을 제공합니다.

반면
다중 상속을 금지하는 C# 에서는
기능의 구현 여부에 따라 다형성을 제공하기 위해 인터페이스를 지원합니다.
이는 일종의 “Duck Typing” 을 제공하기 위한 수단인 것이죠.
(사실상 C# 의 인터페이스는 이 의미가 강합니다.)

따라서 인터페이스는 타입 간 상하/종속 관계의 의미가 아니라
대상 인터페이스가 정의하는 내용을 구현할 경우, 해당 인터페이스 타입으로 인정하겠다… 라는 의미로 사용됩니다.

사실 이거 혼란스러울 수 있다고 생각하는데요.
제가 생각하기에 그 원인 중 하나가 클래스의 상속과 인터페이스의 구현을 표기하는 방식이 동일해서라고 생각해요.

public class Foo : Bar
{
}

public class Foo : IBar
{
}

이렇게 인터페이스 클래스의 다형성 제공 방법이 동일한 방식으로 표시되는 문법이다보니

이 둘을 다 “상속” 이라고 표현하면서 이런 혼란이 생긴다고 보는 편이에요.


엄밀히 말하자면

클래스는 부모의 것을 물려받고 부모인척 하는 방식의 “상속과 파생” 이고
인터페이스는 정한 걸 구현했을 때 같은 타입으로 인정해주겠다는 “계약과 구현” 의 관계라고 봐야합니다.

여기에 추상클래스는 상속의 개념을 유지하면서 계약의 의미를 좀 더 강화한 형태로 제공되는 것이죠.

그런데 이제 인터페이스도… 기본 구현이 들어가면 약간 상속 같은 개념이 추가되었다고 봐야할까 싶긴하네요…

8개의 좋아요

의견

  1. Static 사용은 thread safe 문제에도 영향줍니다.
  2. 컴파일시점에 정의되어 런타임중에 상태가 변할일이 없는놈이라면 static이 요긴합니다.
7개의 좋아요