본문 바로가기

닷컴's_열공/JAVA

RSS Reader 컴포넌트 만들기 - 자바

 

일단, 저번 강좌를 꼼꼼히 읽으신 분은 다음과 같은 일종의 규칙(?)을 알고 계시리라 생각합니다.

1. 하나의 RSS 파일은 하나의 Channel 요소와 여러개의 Item 요소로 구성된다.
2. Channel은 title, link, description, lastBuildDate등의 하위 요소를 가질 수 있다.
3. 각각의 Item 은 Author, Category, Title, Link, Pubdate 등의 하위 요소를 가질 수 있다.

대략 이정도의 규칙만으로도 우리가 같이 제작하려는 RSSReader를 설계하는 데에는 크게 문제가 없을 것 같습니다. 컴포넌트를 많이 만들어 보신 분은 위의 정의만으로도 쉽게 필요한 개체들을 추출해내실 수 있을 것 같습니다.

일단, item 이라는 개체가 필요할 것 같구요. 또한, channel 이라는 개체도 필요할 것 같네요. 그리고, item 개체들은 여러개가 있을 수 있기에, 그들을 그룹핑하기 위한 컬렉션 개체도 하나 있어야 할 것 같습니다. 그리고, 마지막으로 그들은 각각 RssReader 개체를 통해서 접근될 수 있는 속성이기도 해야할 것 같습니다. 

해서, 저는 다음과 같은 구성을 그려보았습니다.

배경색이 하얀 것은 개체이고, 회색인 것은 컬렉션을 의미한다고 보시면 되겠습니다. ^^; 대략적으로 위와같은 구조를 가진다면 그런대로 적절하겠죠?  참고로, 저의 경우는 Reader 개체의 이름은 RssReader로, Channel 개체의 이름은 RssChannel로, item의 이름은 RssItem으로, 그리고 마지막으로 컬렉션은 RssItems 라고 명명하기로 했습니다. 제가 붙인 명칭이 맘에 들지 않는다면, 여러분은 여러분이 원하시는 이름으로 명명하셔도 좋을 듯 합니다. ^^

자. 그럼 제가 각각의 개체들의 코드를 어떻게 작성했는지 보여드리겠습니다. 이 방법이 가장 좋은 방법은 물론 아닐 것입니다. 제가 작성한 코드는 사실 성능을 면밀하게 고려하여 작성된 코드는 아니구요. 그냥 일반적인 수준의 코드일 뿐이니까요. 하지만, 사용해보니 이 정도의 코드로도 괜찮게 동작하기에 저는 이 코드를 사용하고 있습니다(아주 약간의 수정을 해서 말이죠 ^^)

자. 우선 보여드릴 것은 RssItem 개체와 RssChannel 개체의 코드입니다. 참고로, 저의 경우는 RssItem은 구조체로써 작성해 보았구요. RssChannle은 클래스로 작성해 보았습니다. 이들은 단순히 데이터를 보관하기만 하면 되는 녀석들이기에, 아무런 메서드 없이 단순히 필드와 속성으로만 구성되어져 있습니다.

public class RssChannel
{
    private string title;
    private Uri link;
    private string description;
    private string language;
    private string copyright;
    private DateTime lastBuildDate;

    public string Title
    {
    get {    return title;    }
    set {    title = value;    }
    }

    public Uri Link
    {
    get {    return link;    }
    set {    link = value;    }
    }

    public string Description
    {
    get {    return description;    }
    set {    description = value;    }
    }

    public string Language
    {
    get {    return language;    }
    set {    language = value;    }
    }

    public string Copyright
    {
    get {    return copyright;    }
    set {    copyright = value;    }
    }

    public DateTime LastBuildDate
    {
    get {    return lastBuildDate;    }
    set {    lastBuildDate = value;    }
    }
}

public struct RssItem
{
    public string Author;
    public string Category;
    public string Title;
    public Uri Link;
    public DateTime Pubdate;
    public string Description;
}

그렇습니다. RSS 2.0 스펙에 있는 모든 요소들을 다 필드로 작성하지는 않았습니다. 더 완벽한 리더기를 제작하려면 그들의 목록이 모두 들어있어야 하겠지만, 일반적인 수준의 것은 그렇게까지 완벽할 필요가 없어보이더라구요. 사실, 인터넷에 떠돌고 있는 대부분의 RSS가 그 스펙에서 요구하는 모든 요소들을 기록하고 있지도 않은 편이니까요.

해서, 저의 경우는 보편적으로 많이 사용하는 요소들만을 가진 콤팩트한 버전을 만들고 있는 것이랍니다.

위의 코드에서 크게 어려운 점을 없었을 것 같습니다. C#이나 VB.NET을 어느정도 공부했다면 책의 초반부에 나오는 이야기들이니까요.  클래스 정의하기, 구조체 정의하기, 속성 정의하기 등등 말입니다. ^^;

그렇다면, 이어서 코드를 보시겠습니다. 이번 코드는 RssItem을 그룹핑하는 역할을 담당하는 RssImtes 컬렉션 클래스입니다. ^^

public class RssItems : CollectionBase
{
    public RssItem this[int index]
    {
        get
        {
            return (RssItem)List[index];
        }
        set
        {
            List[index] = value;
        }
    }

    public int Add(RssItem value)
    {
        return List.Add(value);
    }

    public int IndexOf(RssItem value)
    {
        return List.IndexOf(value);
    }

    public void Insert(int index, RssItem value)
    {
        List.Insert(index, value);
    }

    public void Remove(RssItem value)
    {
        List.Remove(value);
    }

    public bool Contains(RssItem value)
    {
        return List.Contains(value);
    }
}

RssItems 컬렉션은 CollectionBase라는 클래스로부터 상속을 받고 있는데요. 이 클래스를 사용하면 강력한 형식의 사용자 지정 컬렉션을 쉽게 만들 수 있습니다. 해서, 컬렉션을 제작하고 싶은 경우에 많이들 상속을 받는 클래스입니다. 이는 저의 경우도 예외는 아니어서리.. 저도 CollectionBase 로부터 상속을 받아보았습니다.

CollectionBase 클래스는 내부적으로 IList 인터페이스를 구현하고 있으며, CollectionBase 개체 인스턴스의 요소 목록을 포함하는 List(IList 인터페이스)를 제공하고 있습니다. 해서, CollectionBase 클래스를 상속받아서 자체 컬렉션을 구성하고 싶다면, IList의 멤버들을 구현해주는 것이 좋겠죠? 그러면, 개체가 보기에 더욱 꼴렉숀 스러울 테니까요

그리고, 컬렉션이라면 필수로 필요한 기능인!! 인덱서도 달아주는 게 좋을 것이구요 ^^

이러한 이유로 위와 같이 Add, Insert, Remove, Contains, IndexOf와 같은 낯설지 않은 이름의 메서드들을 정의한 것이랍니다. 그리고, 그 내부 구현을 위해서는 CollectionBase 클래스의 속성인 List를 편리하게 이용하고 있구요 ^^; (이 부분의 내용이 조금 어렵게 받아들여지신다면, .NET 언어에 대한 학습이 조금 필요하신 것 같으니, 추후에 꼭 보충학습 해 주세요 ^^)

자.... 이제 기본적인 하위 개체들의 설계는 어느정도 완성된 것 같습니다. 그렇다면, 이 개체들을 잘 조립해서 이제 본격적인 RssReader 클래스의 설계로 들어가 보겠습니다. ^^;

저의 경우는 일단 RSS(XML 파일)에서 데이터를 읽어와서 그것을 XML 관련 .NET 개체인 XmlDocument에 로드한 다음, 로드된 XML 데이터를 RssItem과 RssChannel에 채우는 식으로 설계해 보았습니다.

RssReader의 대표적인 외부로 공개할 API로는 Load() 메서드와 LoadFromHttp()를 계획하고 있는데요. 이들은 각각 로컬에 있는 RSS 파일 혹은 인터넷 저너머에 존재하는 RSS 파일을 가져와서 이를 로드하는 역할을 수행합니다. 즉, 인자로 지정된 RSS 파일을 읽어와서 그것을 RssReader 개체에 로드하는 역할을 수행하게 할 예정입니다. 그러다보니, 이 두 개의 메서드는 공통적인 작업을 진행하게 되는데요. 그것은 바로, XML 파일로부터 데이터를 가져와서 RssReader 개체의 속성 개체인 Channel과 Items에 채워넣는 부분입니다. 로컬에서 RSS XML 파일을 읽어오던, 웹을 통해서 읽어오던 일단 XML 파일이 XmlDocument 개체에 로드된 후에는 그로부터 데이터를 추출하여 RssReader를 채우는 작업은 의심의 여지없이 동일할테니까요. 해서, 이 부분은 공통되는 부분이기에 내부적으로 별도 함수(PopulateRssData)로 빼서 제작해 보았습니다.

그럼, 조금 복잡해 보일 수도 있겠지만, 한번 제가 작성한 코드를 보시도록 하겠습니다.

public class RssReader
{
    private XmlDocument Document;
    private XmlNode DocumentRoot;
    private RssItems rssItems;
    private RssChannel rssChannel;

    // 생성자. XmlDocument 개체 생성 및 RssItems 컬렉션 생성
    public RssReader()
    {
        Document = new XmlDocument();
        rssItems = new RssItems();
    }

    #region [공개 속성들]
    // RssItems 컬렉션을 속성으로써 노출한다
    public RssItems Items
    {
        get {    return rssItems;    }
        set {    rssItems = value;    }
    }

    // RssChannel 개체를 속성으로써 노출한다
    public RssChannel Channel
    {
        get {    return rssChannel;    }
        set {    rssChannel = value;    }
    }
    #endregion

    #region [공개 메서드들]
    // 외부 공개 메서드. 내부적으로 로컬 XML 파일을 로드하는 LoadFromFile 메서드를 호출하고,
    // 로드된 XML 데이트를 가지고, RssReader 개체를 채우는 PopulateRssData 메서드를 호출한다
    public void Load(string filename)
    {
        LoadFromFile(filename);
        PopulateRssData();
    }

    // 외부 공개 메서드. 내부적으로 원격 XML 파일을 로드하는 LoadFromUrl 메서드를 호출하고,
    // 로드된 XML 데이트를 가지고, RssReader 개체를 채우는 PopulateRssData 메서드를 호출한다
    public void LoadFromHttp(string Url)
    {
        LoadFromUrl(Url);
        PopulateRssData();
    }

    // 로컬 XML 파일을 XmlDocument에 로드한다
    private void LoadFromFile(string filename)
    {
        Document.Load(filename);
    }

    // 원격 XML 파일을 HttpWebRequest, HttpWebResponse를 이용하여 XmlDocument에 로드한다
    private void LoadFromUrl(string Url)
    {
        HttpWebRequest request;
        string responseText = "";

        request = (HttpWebRequest)WebRequest.Create(Url);
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        Stream stream = response.GetResponseStream();

        StreamReader reader = new StreamReader(stream, System.Text.Encoding.GetEncoding(949));
        responseText = reader.ReadToEnd();

        response.Close();
        reader.Close();

        Document.LoadXml(responseText);
    }

    #endregion

    // 지정된 XmlNodeList에서 특정 이름의 노드를 찾아서 반환한다
    private XmlNode getNode(XmlNodeList list, string nodeName)
    {
        for (int i = 0; i < = list.Count - 1; i++)
        {
            if (list.Item(i).Name == nodeName)
            {
                return list.Item(i);
            }
        }

        return null;
    }

    // XmlDocument에서 데이터를 추출하여 RssReader 개체를 채운다
    private void PopulateRssData()
    {
        XmlNode node;
        XmlNode itemNode;

        //헤더 초기화
        rssChannel = new RssChannel();
        rssChannel.Copyright = "";
        rssChannel.Description = "";
        rssChannel.Language = "";
        rssChannel.Title = "";

        DocumentRoot = getNode(Document.ChildNodes, "rss");
        DocumentRoot = getNode(DocumentRoot.ChildNodes, "channel");

        // 헤더값들을 설정하는 부분
        node = getNode(DocumentRoot.ChildNodes, "title");
        if (node != null) rssChannel.Title = node.InnerText;
        node = getNode(DocumentRoot.ChildNodes, "link");
        if (node != null) rssChannel.Link = new Uri(node.InnerText);
        node = getNode(DocumentRoot.ChildNodes, "description");
        if (node != null) rssChannel.Description = node.InnerText;
        node = getNode(DocumentRoot.ChildNodes, "language");
        if (node != null) rssChannel.Language = node.InnerText;
        node = getNode(DocumentRoot.ChildNodes, "copyright");
        if (node != null) rssChannel.Copyright = node.InnerText;
        node = getNode(DocumentRoot.ChildNodes, "lastBuildDate");
        if (node != null) rssChannel.LastBuildDate= DateTime.Parse(node.InnerText);

        rssItems.Clear();
        for (int i = 0; i < = DocumentRoot.ChildNodes.Count - 1; i++)
        {
            // item 노드를 루프 돌면서 데이터를 가져온다
            if (DocumentRoot.ChildNodes[i].Name == "item")
            {
                itemNode = DocumentRoot.ChildNodes[i];
                RssItem item = new RssItem();

                node = getNode(itemNode.ChildNodes, "author");
                if (node != null) item.Author = node.InnerText;

                node = getNode(itemNode.ChildNodes, "category");
                if (node != null) item.Category = node.InnerText;

                node = getNode(itemNode.ChildNodes, "title");
                if (node != null) item.Title = node.InnerText;

                node = getNode(itemNode.ChildNodes, "link");
                if (node != null) item.Link = new Uri(node.InnerText);

                node = getNode(itemNode.ChildNodes, "pubDate");
                if (node != null) item.Pubdate = DateTime.Parse(node.InnerText);

                node = getNode(itemNode.ChildNodes, "description");
                if (node != null) item.Description = node.InnerText;

                rssItems.Add(item);
            }
        }
    }
}

조금만 신중하게 살펴보시면 충분히 이해가 될만한 코드인 것 같네요 ^^;; 그쵸? (얼렁뚱땅 같이 넘어갑니당~ 히히)

자. 모두 작성하였으면 이제 한번 이 클래스를 컴파일 해볼까요? 저의 경운, 별도의 클래스 라이브러리 프로젝트를 만들었구요. RssReader.cs 라는 클래스 파일을 만들어서 위와 같이 코드를 작성해 보았답니다. ^^

앗. 혹시 컴파일시 에러가 나신다는 분들을 위해서 임포트해야 할 네임스페이스도 친절하게 알려드리겠습니다. 바로 다음과 같습니다.

using System;
using System.Collections;
using System.Data;
using System.Web;
using System.IO;
using System.Xml;
using System.Net;

그래도, 에러가 나신 다는 분들이 계시는 것 같네요. 그렇다면, 그분들을 위해서 아예 소스 파일을 드리겠습니다. 다운로드 받아주세요 ^^;;

RssReader가 포함된 클래스 라이브러리 다운로드

참고로, 이 프로젝트는 VS.NET 2003으로 제작되었습니다.
VS.NET 하위버전을 쓰시는 분은 클래스 파일만 가져다가 사용하세요

이제 컴파일도 되었고, RssReader 컴포넌트도 완성이 되었죠? 그렇다면, 이 녀석을 직접 한번 사용해 보도록 해요 ^^ 여러분의 웹 프로젝트에다가 빈 웹 폼 페이지를 하나 추가해 주세요. 어떤 이름의 aspx 여도 무방합니다. ^^; 그리고, 그 웹폼의 Page_Load 이벤트에 다음과 같이 코드를 작성해 보세요 ^^

    private void Page_Load(object sender, System.EventArgs e)
    {
        // 여기에 사용자 코드를 배치하여 페이지를 초기화합니다.
        RssReader reader = new RssReader();
        reader.LoadFromHttp("http://www.taeyo.net/lecture/rss.xml");

        RssChannel head = reader.Channel;

        Response.Write("Title : " + reader.Channel.Title + "<BR>");
        Response.Write("Link : " + reader.Channel.Link.ToString() + "<BR>");
        Response.Write("Language : " + reader.Channel.Language + "<BR>");
        Response.Write("lastBuildDate : " + reader.Channel.LastBuildDate + "<BR>");
        Response.Write("Description : " + reader.Channel.Description + "<BR><BR>");

        for(int i = 0; i < reader.Items.Count; i++)
        {
            RssItem item = reader.Items[i];
            Response.Write(item.Author + " : " + item.Title + " (" + item.Pubdate.ToString() +")<BR>");
        }
    }

그리고, 이 웹 페이지를 실행해 보아요. 물론, 실행 전에 앞에서 만든 TaeyoWebLib.dll을 프로젝트에서 참조하고 있어야 하고, using TaeyoWebLib를 통해서 네임스페이스를 임포트해야하는 것은 잊지 않으셨어야 제대로 실행될 것입니다. ^^

어떤가요? 멋지게 다른 웹 사이트의 RSS를 가져와서 출력해주고 있나요? 태오 사이트가 아닌 다른 사이트의 RSS도 한번 읽어와 보세요. 어느정도 만족할만한 수준으로 동작하지 않나요? 히히... 재미있죠?

사실, 실제 제가 사용하는 RssReader 클래스의 경우에는 조금 더 양념을 쳐 보았는데요. 그러니깐, 고백하자면 저의 경우는 RssItems 컬렉션의 데이터를 DataSet으로 반환하는 메서드를 추가로 넣어두었습니다. 해서, 쉽게 DataGrid와 같은 바운드 컨트롤에 쉽게 바인드가 가능하게끔 말이지요 ^^;; 이 부분은 여러분이 한번 스스로 추가해 보시겠어요? 그리고, 만일, 도저히 안되겠다 하시는 분들을 위해서는 힌트를 드릴께요. 그것은... 현재 페이지에서 [소스보기]를 하시면 됩니다. 힌트는 여기까지~~ ^^

그리고, 이러한 컴포넌트를 사용할 경우 조심해야 할 사항이 있어서 알려드리는데요. 그것은 RSS를 통해서 데이터를 읽어와서 자신의 웹 사이트에서 무단으로 링크를 걸어서는 안된다는 것입니다. 예를 들어서, 여러분이 태오사이트에서 제공하는 RSS를 읽어서, 여러분의 사이트에서 그 리스트를 나열하고 링크를 걸고 싶다고 해도, 그렇게 하려면 우선적으로 해당 사이트의 관리자에게 동의를 받으셔야 합니다. 만일, 그렇게 하지 않을 경우에는 차후 어떠한 불이익이 올지 모르니까요.

새로운 태오닷넷 사이트의 메인에는 Microsoft가 제공하는 MSDN의 목록을 링크목록으로 출력하는 것을 보실 수 있을텐데요. 그 부분이 바로 지금 같이 만들어 본 RssReader를 사용해서 구현한 것입니다만, 저의 경우는 그것을 링크하기 위해서 Microsoft에 사용동의 요청을 했었구요. 보름 정도의 시간이 걸려서 결국은 승낙을 얻어냈기에 링크를 하고 있는 것이랍니다. (보름은 내게 너무 길었어.. ㅠㅠ 하지만, 다행이야.. )